mabuseif commited on
Commit
9b5e232
·
verified ·
1 Parent(s): 41a0d3e

Upload 4 files

Browse files
Files changed (3) hide show
  1. app/data_export.py +558 -611
  2. app/data_persistence.py +1 -17
  3. app/main.py +472 -455
app/data_export.py CHANGED
@@ -13,33 +13,11 @@ import base64
13
  import io
14
  from datetime import datetime
15
  import xlsxwriter
16
- import logging
17
 
18
- # Configure logging
19
- logging.basicConfig(level=logging.INFO)
20
- logger = logging.getLogger(__name__)
21
 
22
  class DataExport:
23
  """Class for data export functionality."""
24
 
25
- def __init__(self):
26
- """Initialize DataExport."""
27
- logger.info("Initializing DataExport")
28
-
29
- def display(self, session_state: Dict[str, Any]) -> None:
30
- """
31
- Display the export interface in Streamlit.
32
-
33
- Args:
34
- session_state: Streamlit session state containing calculation results
35
- """
36
- try:
37
- logger.info("Displaying export interface")
38
- self.display_export_interface(session_state)
39
- except Exception as e:
40
- logger.error(f"Error in display method: {str(e)}")
41
- st.error(f"Failed to display export interface: {str(e)}")
42
-
43
  @staticmethod
44
  def export_to_csv(data: Dict[str, Any], file_path: str = None) -> Optional[str]:
45
  """
@@ -68,8 +46,7 @@ class DataExport:
68
  return csv_data
69
 
70
  except Exception as e:
71
- logger.error(f"Error exporting to CSV: {str(e)}")
72
- st.error(f"Error exporting to CSV: {str(e)}")
73
  return None
74
 
75
  @staticmethod
@@ -116,8 +93,7 @@ class DataExport:
116
  return None
117
 
118
  except Exception as e:
119
- logger.error(f"Error exporting to Excel: {str(e)}")
120
- st.error(f"Error exporting to Excel: {str(e)}")
121
  return None
122
 
123
  @staticmethod
@@ -146,8 +122,7 @@ class DataExport:
146
  return json_data
147
 
148
  except Exception as e:
149
- logger.error(f"Error exporting scenario to JSON: {str(e)}")
150
- st.error(f"Error exporting scenario to JSON: {str(e)}")
151
  return None
152
 
153
  @staticmethod
@@ -164,18 +139,13 @@ class DataExport:
164
  Returns:
165
  HTML string with download link
166
  """
167
- try:
168
- if isinstance(data, str):
169
- b64 = base64.b64encode(data.encode()).decode()
170
- else:
171
- b64 = base64.b64encode(data).decode()
172
-
173
- href = f'<a href="data:{mime_type};base64,{b64}" download="{filename}">{text}</a>'
174
- return href
175
- except Exception as e:
176
- logger.error(f"Error generating download link: {str(e)}")
177
- st.error(f"Error generating download link: {str(e)}")
178
- return ""
179
 
180
  @staticmethod
181
  def create_cooling_load_dataframes(results: Dict[str, Any]) -> Dict[str, pd.DataFrame]:
@@ -188,175 +158,170 @@ class DataExport:
188
  Returns:
189
  Dictionary with DataFrames for Excel export
190
  """
191
- try:
192
- dataframes = {}
193
-
194
- # Create summary DataFrame
195
- summary_data = {
196
- "Metric": [
197
- "Total Cooling Load",
198
- "Sensible Cooling Load",
199
- "Latent Cooling Load",
200
- "Cooling Load per Area"
201
- ],
202
- "Value": [
203
- results["cooling"]["total_load"],
204
- results["cooling"]["sensible_load"],
205
- results["cooling"]["latent_load"],
206
- results["cooling"]["load_per_area"]
207
- ],
208
- "Unit": [
209
- "kW",
210
- "kW",
211
- "kW",
212
- "W/m²"
213
- ]
214
- }
215
-
216
- dataframes["Cooling Summary"] = pd.DataFrame(summary_data)
217
-
218
- # Create component breakdown DataFrame
219
- component_data = {
220
- "Component": [
221
- "Walls",
222
- "Roof",
223
- "Windows",
224
- "Doors",
225
- "People",
226
- "Lighting",
227
- "Equipment",
228
- "Infiltration",
229
- "Ventilation"
230
- ],
231
- "Load (kW)": [
232
- results["cooling"]["component_loads"]["walls"],
233
- results["cooling"]["component_loads"]["roof"],
234
- results["cooling"]["component_loads"]["windows"],
235
- results["cooling"]["component_loads"]["doors"],
236
- results["cooling"]["component_loads"]["people"],
237
- results["cooling"]["component_loads"]["lighting"],
238
- results["cooling"]["component_loads"]["equipment"],
239
- results["cooling"]["component_loads"]["infiltration"],
240
- results["cooling"]["component_loads"]["ventilation"]
241
- ],
242
- "Percentage (%)": [
243
- results["cooling"]["component_loads"]["walls"] / results["cooling"]["total_load"] * 100,
244
- results["cooling"]["component_loads"]["roof"] / results["cooling"]["total_load"] * 100,
245
- results["cooling"]["component_loads"]["windows"] / results["cooling"]["total_load"] * 100,
246
- results["cooling"]["component_loads"]["doors"] / results["cooling"]["total_load"] * 100,
247
- results["cooling"]["component_loads"]["people"] / results["cooling"]["total_load"] * 100,
248
- results["cooling"]["component_loads"]["lighting"] / results["cooling"]["total_load"] * 100,
249
- results["cooling"]["component_loads"]["equipment"] / results["cooling"]["total_load"] * 100,
250
- results["cooling"]["component_loads"]["infiltration"] / results["cooling"]["total_load"] * 100,
251
- results["cooling"]["component_loads"]["ventilation"] / results["cooling"]["total_load"] * 100
252
- ]
253
- }
254
-
255
- dataframes["Cooling Components"] = pd.DataFrame(component_data)
256
-
257
- # Create detailed loads DataFrames
258
-
259
- # Walls
260
- wall_data = []
261
- for wall in results["cooling"]["detailed_loads"]["walls"]:
262
- wall_data.append({
263
- "Name": wall["name"],
264
- "Orientation": wall["orientation"],
265
- "Area (m²)": wall["area"],
266
- "U-Value (W/m²·K)": wall["u_value"],
267
- "CLTD (°C)": wall["cltd"],
268
- "Load (kW)": wall["load"]
269
- })
270
-
271
- if wall_data:
272
- dataframes["Cooling Walls"] = pd.DataFrame(wall_data)
273
-
274
- # Roofs
275
- roof_data = []
276
- for roof in results["cooling"]["detailed_loads"]["roofs"]:
277
- roof_data.append({
278
- "Name": roof["name"],
279
- "Orientation": roof["orientation"],
280
- "Area (m²)": roof["area"],
281
- "U-Value (W/m²·K)": wall["u_value"],
282
- "CLTD (°C)": roof["cltd"],
283
- "Load (kW)": roof["load"]
284
- })
285
-
286
- if roof_data:
287
- dataframes["Cooling Roofs"] = pd.DataFrame(roof_data)
288
-
289
- # Windows
290
- window_data = []
291
- for window in results["cooling"]["detailed_loads"]["windows"]:
292
- window_data.append({
293
- "Name": window["name"],
294
- "Orientation": wall["orientation"],
295
- "Area (m²)": wall["area"],
296
- "U-Value (W/m²·K)": wall["u_value"],
297
- "SHGC": window["shgc"],
298
- "SCL (W/m²)": window["scl"],
299
- "Load (kW)": window["load"]
300
- })
301
-
302
- if window_data:
303
- dataframes["Cooling Windows"] = pd.DataFrame(window_data)
304
-
305
- # Doors
306
- door_data = []
307
- for door in results["cooling"]["detailed_loads"]["doors"]:
308
- door_data.append({
309
- "Name": door["name"],
310
- "Orientation": door["orientation"],
311
- "Area (m²)": door["area"],
312
- "U-Value (W/m²·K)": door["u_value"],
313
- "CLTD (°C)": door["cltd"],
314
- "Load (kW)": door["load"]
315
- })
316
-
317
- if door_data:
318
- dataframes["Cooling Doors"] = pd.DataFrame(door_data)
319
-
320
- # Internal loads
321
- internal_data = []
322
- for internal_load in results["cooling"]["detailed_loads"]["internal"]:
323
- internal_data.append({
324
- "Type": internal_load["type"],
325
- "Name": internal_load["name"],
326
- "Quantity": internal_load["quantity"],
327
- "Heat Gain (W)": internal_load["heat_gain"],
328
- "CLF": internal_load["clf"],
329
- "Load (kW)": internal_load["load"]
330
- })
331
-
332
- if internal_data:
333
- dataframes["Cooling Internal Loads"] = pd.DataFrame(internal_data)
334
-
335
- # Infiltration and ventilation
336
- air_data = [
337
- {
338
- "Type": "Infiltration",
339
- "Air Flow (m³/s)": results["cooling"]["detailed_loads"]["infiltration"]["air_flow"],
340
- "Sensible Load (kW)": results["cooling"]["detailed_loads"]["infiltration"]["sensible_load"],
341
- "Latent Load (kW)": results["cooling"]["detailed_loads"]["infiltration"]["latent_load"],
342
- "Total Load (kW)": results["cooling"]["detailed_loads"]["infiltration"]["total_load"]
343
- },
344
- {
345
- "Type": "Ventilation",
346
- "Air Flow (m³/s)": results["cooling"]["detailed_loads"]["ventilation"]["air_flow"],
347
- "Sensible Load (kW)": results["cooling"]["detailed_loads"]["ventilation"]["sensible_load"],
348
- "Latent Load (kW)": results["cooling"]["detailed_loads"]["ventilation"]["latent_load"],
349
- "Total Load (kW)": results["cooling"]["detailed_loads"]["ventilation"]["total_load"]
350
- }
351
  ]
352
-
353
- dataframes["Cooling Air Exchange"] = pd.DataFrame(air_data)
354
-
355
- return dataframes
356
- except Exception as e:
357
- logger.error(f"Error creating cooling load dataframes: {str(e)}")
358
- st.error(f"Error creating cooling load dataframes: {str(e)}")
359
- return {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
 
361
  @staticmethod
362
  def create_heating_load_dataframes(results: Dict[str, Any]) -> Dict[str, pd.DataFrame]:
@@ -369,165 +334,160 @@ class DataExport:
369
  Returns:
370
  Dictionary with DataFrames for Excel export
371
  """
372
- try:
373
- dataframes = {}
374
-
375
- # Create summary DataFrame
376
- summary_data = {
377
- "Metric": [
378
- "Total Heating Load",
379
- "Heating Load per Area",
380
- "Design Heat Loss",
381
- "Safety Factor"
382
- ],
383
- "Value": [
384
- results["heating"]["total_load"],
385
- results["heating"]["load_per_area"],
386
- results["heating"]["design_heat_loss"],
387
- results["heating"]["safety_factor"]
388
- ],
389
- "Unit": [
390
- "kW",
391
- "W/m²",
392
- "kW",
393
- "%"
394
- ]
395
- }
396
-
397
- dataframes["Heating Summary"] = pd.DataFrame(summary_data)
398
-
399
- # Create component breakdown DataFrame
400
- component_data = {
401
- "Component": [
402
- "Walls",
403
- "Roof",
404
- "Floor",
405
- "Windows",
406
- "Doors",
407
- "Infiltration",
408
- "Ventilation"
409
- ],
410
- "Load (kW)": [
411
- results["heating"]["component_loads"]["walls"],
412
- results["heating"]["component_loads"]["roof"],
413
- results["heating"]["component_loads"]["floor"],
414
- results["heating"]["component_loads"]["windows"],
415
- results["heating"]["component_loads"]["doors"],
416
- results["heating"]["component_loads"]["infiltration"],
417
- results["heating"]["component_loads"]["ventilation"]
418
- ],
419
- "Percentage (%)": [
420
- results["heating"]["component_loads"]["walls"] / results["heating"]["total_load"] * 100,
421
- results["heating"]["component_loads"]["roof"] / results["heating"]["total_load"] * 100,
422
- results["heating"]["component_loads"]["floor"] / results["heating"]["total_load"] * 100,
423
- results["heating"]["component_loads"]["windows"] / results["heating"]["total_load"] * 100,
424
- results["heating"]["component_loads"]["doors"] / results["heating"]["total_load"] * 100,
425
- results["heating"]["component_loads"]["infiltration"] / results["heating"]["total_load"] * 100,
426
- results["heating"]["component_loads"]["ventilation"] / results["heating"]["total_load"] * 100
427
- ]
428
- }
429
-
430
- dataframes["Heating Components"] = pd.DataFrame(component_data)
431
-
432
- # Create detailed loads DataFrames
433
-
434
- # Walls
435
- wall_data = []
436
- for wall in results["heating"]["detailed_loads"]["walls"]:
437
- wall_data.append({
438
- "Name": wall["name"],
439
- "Orientation": wall["orientation"],
440
- "Area (m²)": wall["area"],
441
- "U-Value (W/m²·K)": wall["u_value"],
442
- "Temperature Difference (°C)": wall["delta_t"],
443
- "Load (kW)": wall["load"]
444
- })
445
-
446
- if wall_data:
447
- dataframes["Heating Walls"] = pd.DataFrame(wall_data)
448
-
449
- # Roofs
450
- roof_data = []
451
- for roof in results["heating"]["detailed_loads"]["roofs"]:
452
- roof_data.append({
453
- "Name": roof["name"],
454
- "Orientation": roof["orientation"],
455
- "Area (m²)": roof["area"],
456
- "U-Value (W/m²·K)": roof["u_value"],
457
- "Temperature Difference (°C)": roof["delta_t"],
458
- "Load (kW)": roof["load"]
459
- })
460
-
461
- if roof_data:
462
- dataframes["Heating Roofs"] = pd.DataFrame(roof_data)
463
-
464
- # Floors
465
- floor_data = []
466
- for floor in results["heating"]["detailed_loads"]["floors"]:
467
- floor_data.append({
468
- "Name": floor["name"],
469
- "Area (m²)": floor["area"],
470
- "U-Value (W/m²·K)": floor["u_value"],
471
- "Temperature Difference (°C)": floor["delta_t"],
472
- "Load (kW)": floor["load"]
473
- })
474
-
475
- if floor_data:
476
- dataframes["Heating Floors"] = pd.DataFrame(floor_data)
477
-
478
- # Windows
479
- window_data = []
480
- for window in results["heating"]["detailed_loads"]["windows"]:
481
- window_data.append({
482
- "Name": window["name"],
483
- "Orientation": window["orientation"],
484
- "Area (m²)": window["area"],
485
- "U-Value (W/m²·K)": window["u_value"],
486
- "Temperature Difference (°C)": window["delta_t"],
487
- "Load (kW)": window["load"]
488
- })
489
-
490
- if window_data:
491
- dataframes["Heating Windows"] = pd.DataFrame(window_data)
492
-
493
- # Doors
494
- door_data = []
495
- for door in results["heating"]["detailed_loads"]["doors"]:
496
- door_data.append({
497
- "Name": door["name"],
498
- "Orientation": door["orientation"],
499
- "Area (m²)": door["area"],
500
- "U-Value (W/m²·K)": door["u_value"],
501
- "Temperature Difference (°C)": door["delta_t"],
502
- "Load (kW)": door["load"]
503
- })
504
-
505
- if door_data:
506
- dataframes["Heating Doors"] = pd.DataFrame(door_data)
507
-
508
- # Infiltration and ventilation
509
- air_data = [
510
- {
511
- "Type": "Infiltration",
512
- "Air Flow (m³/s)": results["heating"]["detailed_loads"]["infiltration"]["air_flow"],
513
- "Temperature Difference (°C)": results["heating"]["detailed_loads"]["infiltration"]["delta_t"],
514
- "Load (kW)": results["heating"]["detailed_loads"]["infiltration"]["load"]
515
- },
516
- {
517
- "Type": "Ventilation",
518
- "Air Flow (m³/s)": results["heating"]["detailed_loads"]["ventilation"]["air_flow"],
519
- "Temperature Difference (°C)": results["heating"]["detailed_loads"]["ventilation"]["delta_t"],
520
- "Load (kW)": results["heating"]["detailed_loads"]["ventilation"]["load"]
521
- }
522
  ]
523
-
524
- dataframes["Heating Air Exchange"] = pd.DataFrame(air_data)
525
-
526
- return dataframes
527
- except Exception as e:
528
- logger.error(f"Error creating heating load dataframes: {str(e)}")
529
- st.error(f"Error creating heating load dataframes: {str(e)}")
530
- return {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
531
 
532
  @staticmethod
533
  def display_export_interface(session_state: Dict[str, Any]) -> None:
@@ -537,28 +497,24 @@ class DataExport:
537
  Args:
538
  session_state: Streamlit session state containing calculation results
539
  """
540
- try:
541
- st.header("Export Results")
542
-
543
- # Check if calculations have been performed
544
- if "calculation_results" not in session_state or not session_state["calculation_results"]:
545
- st.warning("No calculation results available. Please run calculations first.")
546
- return
547
-
548
- # Create tabs for different export options
549
- tab1, tab2, tab3 = st.tabs(["CSV Export", "Excel Export", "Scenario Export"])
550
-
551
- with tab1:
552
- DataExport._display_csv_export(session_state)
553
-
554
- with tab2:
555
- DataExport._display_excel_export(session_state)
556
-
557
- with tab3:
558
- DataExport._display_scenario_export(session_state)
559
- except Exception as e:
560
- logger.error(f"Error displaying export interface: {str(e)}")
561
- st.error(f"Error displaying export interface: {str(e)}")
562
 
563
  @staticmethod
564
  def _display_csv_export(session_state: Dict[str, Any]) -> None:
@@ -568,49 +524,45 @@ class DataExport:
568
  Args:
569
  session_state: Streamlit session state containing calculation results
570
  """
571
- try:
572
- st.subheader("CSV Export")
573
-
574
- # Get results
575
- results = session_state["calculation_results"]
576
-
577
- # Create tabs for cooling and heating loads
578
- tab1, tab2 = st.tabs(["Cooling Load CSV", "Heating Load CSV"])
579
-
580
- with tab1:
581
- # Create cooling load DataFrames
582
- cooling_dfs = DataExport.create_cooling_load_dataframes(results)
 
 
 
 
583
 
584
- # Display and export each DataFrame
585
- for sheet_name, df in cooling_dfs.items():
586
- st.write(f"### {sheet_name}")
587
- st.dataframe(df)
588
-
589
- # Add download button
590
- csv_data = DataExport.export_to_csv(df)
591
- if csv_data:
592
- filename = f"{sheet_name.replace(' ', '_').lower()}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
593
- download_link = DataExport.get_download_link(csv_data, filename, f"Download {sheet_name} CSV")
594
- st.markdown(download_link, unsafe_allow_html=True)
595
-
596
- with tab2:
597
- # Create heating load DataFrames
598
- heating_dfs = DataExport.create_heating_load_dataframes(results)
599
 
600
- # Display and export each DataFrame
601
- for sheet_name, df in heating_dfs.items():
602
- st.write(f"### {sheet_name}")
603
- st.dataframe(df)
604
-
605
- # Add download button
606
- csv_data = DataExport.export_to_csv(df)
607
- if csv_data:
608
- filename = f"{sheet_name.replace(' ', '_').lower()}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
609
- download_link = DataExport.get_download_link(csv_data, filename, f"Download {sheet_name} CSV")
610
- st.markdown(download_link, unsafe_allow_html=True)
611
- except Exception as e:
612
- logger.error(f"Error in CSV export interface: {str(e)}")
613
- st.error(f"Error in CSV export interface: {str(e)}")
614
 
615
  @staticmethod
616
  def _display_excel_export(session_state: Dict[str, Any]) -> None:
@@ -620,113 +572,109 @@ class DataExport:
620
  Args:
621
  session_state: Streamlit session state containing calculation results
622
  """
623
- try:
624
- st.subheader("Excel Export")
625
-
626
- # Get results
627
- results = session_state["calculation_results"]
628
-
629
- # Create tabs for cooling, heating, and combined loads
630
- tab1, tab2, tab3 = st.tabs(["Cooling Load Excel", "Heating Load Excel", "Combined Excel"])
631
-
632
- with tab1:
633
- # Create cooling load DataFrames
634
- cooling_dfs = DataExport.create_cooling_load_dataframes(results)
635
-
636
- # Add download button
637
- excel_data = DataExport.export_to_excel(cooling_dfs)
638
- if excel_data:
639
- filename = f"cooling_load_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
640
- download_link = DataExport.get_download_link(
641
- excel_data,
642
- filename,
643
- "Download Cooling Load Excel",
644
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
645
- )
646
- st.markdown(download_link, unsafe_allow_html=True)
647
-
648
- # Display preview
649
- st.write("### Excel Preview")
650
- st.write("The Excel file will contain the following sheets:")
651
- for sheet_name in cooling_dfs.keys():
652
- st.write(f"- {sheet_name}")
653
-
654
- with tab2:
655
- # Create heating load DataFrames
656
- heating_dfs = DataExport.create_heating_load_dataframes(results)
657
-
658
- # Add download button
659
- excel_data = DataExport.export_to_excel(heating_dfs)
660
- if excel_data:
661
- filename = f"heating_load_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
662
- download_link = DataExport.get_download_link(
663
- excel_data,
664
- filename,
665
- "Download Heating Load Excel",
666
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
667
- )
668
- st.markdown(download_link, unsafe_allow_html=True)
669
-
670
- # Display preview
671
- st.write("### redeemed Excel Preview")
672
- st.write("The Excel file will contain the following sheets:")
673
- for sheet_name in heating_dfs.keys():
674
- st.write(f"- {sheet_name}")
675
-
676
- with tab3:
677
- # Create combined DataFrames
678
- combined_dfs = {}
679
-
680
- # Add project information
681
- if "building_info" in session_state:
682
- project_info = [
683
- {"Parameter": "Project Name", "Value": session_state["building_info"].get("project_name", "")},
684
- {"Parameter": "Building Name", "Value": session_state["building_info"].get("building_name", "")},
685
- {"Parameter": "Location", "Value": session_state["building_info"].get("location", "")},
686
- {"Parameter": "Climate Zone", "Value": session_state["building_info"].get("climate_zone", "")},
687
- {"Parameter": "Building Type", "Value": session_state["building_info"].get("building_type", "")},
688
- {"Parameter": "Floor Area", "Value": session_state["building_info"].get("floor_area", "")},
689
- {"Parameter": "Number of Floors", "Value": session_state["building_info"].get("num_floors", "")},
690
- {"Parameter": "Floor Height", "Value": session_state["building_info"].get("floor_height", "")},
691
- {"Parameter": "Orientation", "Value": session_state["building_info"].get("orientation", "")},
692
- {"Parameter": "Occupancy", "Value": session_state["building_info"].get("occupancy", "")},
693
- {"Parameter": "Operating Hours", "Value": session_state["building_info"].get("operating_hours", "")},
694
- {"Parameter": "Date", "Value": datetime.now().strftime("%Y-%m-%d")},
695
- {"Parameter": "Time", "Value": datetime.now().strftime("%H:%M:%S")}
696
- ]
697
-
698
- combined_dfs["Project Information"] = pd.DataFrame(project_info)
699
-
700
- # Add cooling load DataFrames
701
- cooling_dfs = DataExport.create_cooling_load_dataframes(results)
702
- for sheet_name, df in cooling_dfs.items():
703
- combined_dfs[sheet_name] = df
704
-
705
- # Add heating load DataFrames
706
- heating_dfs = DataExport.create_heating_load_dataframes(results)
707
- for sheet_name, df in heating_dfs.items():
708
- combined_dfs[sheet_name] = df
709
-
710
- # Add download button
711
- excel_data = DataExport.export_to_excel(combined_dfs)
712
- if excel_data:
713
- filename = f"hvac_load_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
714
- download_link = DataExport.get_download_link(
715
- excel_data,
716
- filename,
717
- "Download Combined Excel Report",
718
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
719
- )
720
- st.markdown(download_link, unsafe_allow_html=True)
721
 
722
- # Display preview
723
- st.write("### Excel Preview")
724
- st.write("The Excel file will contain the following sheets:")
725
- for sheet_name in combined_dfs.keys():
726
- st.write(f"- {sheet_name}")
727
- except Exception as e:
728
- logger.error(f"Error in Excel export interface: {str(e)}")
729
- st.error(f"Error in Excel export interface: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
730
 
731
  @staticmethod
732
  def _display_scenario_export(session_state: Dict[str, Any]) -> None:
@@ -736,90 +684,90 @@ class DataExport:
736
  Args:
737
  session_state: Streamlit session state containing calculation results
738
  """
739
- try:
740
- st.subheader("Scenario Export")
 
 
 
741
 
742
- # Check if there are saved scenarios
743
- if "saved_scenarios" not in session_state or not session_state["saved_scenarios"]:
744
- st.info("No saved scenarios available for export. Save the current results as a scenario to enable export.")
745
-
746
- # Add button to save current results as a scenario
747
- scenario_name = st.text_input("Scenario Name", value="Baseline")
748
 
749
- if st.button("Save Current Results as Scenario"):
750
- if "saved_scenarios" not in session_state:
751
- session_state["saved_scenarios"] = {}
752
-
753
- # Save current results as a scenario
754
- session_state["saved_scenarios"][scenario_name] = {
755
- "results": session_state["calculation_results"],
756
- "building_info": session_state["building_info"],
757
- "components": session_state["components"],
758
- "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
759
- }
760
-
761
- st.success(f"Scenario '{scenario_name}' saved successfully!")
762
- st.experimental_rerun()
763
- else:
764
- # Display saved scenarios
765
- st.write("### Saved Scenarios")
766
 
767
- # Create selectbox for scenarios
768
- scenario_names = list(session_state["saved_scenarios"].keys())
769
- selected_scenario = st.selectbox("Select Scenario to Export", scenario_names)
 
 
 
 
 
 
 
 
 
 
770
 
771
- if selected_scenario:
772
- # Get selected scenario
773
- scenario = session_state["saved_scenarios"][selected_scenario]
774
-
775
- # Display scenario information
776
- st.write(f"**Scenario:** {selected_scenario}")
777
- st.write(f"**Timestamp:** {scenario['timestamp']}")
778
-
779
- # Add download button
780
- json_data = DataExport.export_scenario_to_json(scenario)
781
- if json_data:
782
- filename = f"{selected_scenario.replace(' ', '_').lower()}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
783
- download_link = DataExport.get_download_link(
784
- json_data,
785
- filename,
786
- "Download Scenario JSON",
787
- "application/json"
788
- )
789
- st.markdown(download_link, unsafe_allow_html=True)
790
 
791
- # Add button to export all scenarios
792
- if st.button("Export All Scenarios"):
793
- # Create a zip file in memory
794
- import zipfile
795
- from io import BytesIO
796
-
797
- zip_buffer = BytesIO()
798
- with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
799
- for scenario_name, scenario in session_state["saved_scenarios"].items():
800
- # Export scenario to JSON
801
- json_data = DataExport.export_scenario_to_json(scenario)
802
- if json_data:
803
- filename = f"{scenario_name.replace(' ', '_').lower()}.json"
804
- zip_file.writestr(filename, json_data)
805
-
806
- # Add download button for zip file
807
- zip_buffer.seek(0)
808
- zip_data = zip_buffer.getvalue()
809
-
810
- filename = f"all_scenarios_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
811
  download_link = DataExport.get_download_link(
812
- zip_data,
813
  filename,
814
- "Download All Scenarios (ZIP)",
815
- "application/zip"
816
  )
817
  st.markdown(download_link, unsafe_allow_html=True)
818
- except Exception as e:
819
- logger.error(f"Error in scenario export interface: {str(e)}")
820
- st.error(f"Error in scenario export interface: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
821
 
 
 
822
 
 
823
  if __name__ == "__main__":
824
  import streamlit as st
825
 
@@ -918,6 +866,5 @@ if __name__ == "__main__":
918
  }
919
  }
920
 
921
- # Create instance and display export interface
922
- data_export = DataExport()
923
- data_export.display(st.session_state)
 
13
  import io
14
  from datetime import datetime
15
  import xlsxwriter
 
16
 
 
 
 
17
 
18
  class DataExport:
19
  """Class for data export functionality."""
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  @staticmethod
22
  def export_to_csv(data: Dict[str, Any], file_path: str = None) -> Optional[str]:
23
  """
 
46
  return csv_data
47
 
48
  except Exception as e:
49
+ st.error(f"Error exporting to CSV: {e}")
 
50
  return None
51
 
52
  @staticmethod
 
93
  return None
94
 
95
  except Exception as e:
96
+ st.error(f"Error exporting to Excel: {e}")
 
97
  return None
98
 
99
  @staticmethod
 
122
  return json_data
123
 
124
  except Exception as e:
125
+ st.error(f"Error exporting scenario to JSON: {e}")
 
126
  return None
127
 
128
  @staticmethod
 
139
  Returns:
140
  HTML string with download link
141
  """
142
+ if isinstance(data, str):
143
+ b64 = base64.b64encode(data.encode()).decode()
144
+ else:
145
+ b64 = base64.b64encode(data).decode()
146
+
147
+ href = f'<a href="data:{mime_type};base64,{b64}" download="{filename}">{text}</a>'
148
+ return href
 
 
 
 
 
149
 
150
  @staticmethod
151
  def create_cooling_load_dataframes(results: Dict[str, Any]) -> Dict[str, pd.DataFrame]:
 
158
  Returns:
159
  Dictionary with DataFrames for Excel export
160
  """
161
+ dataframes = {}
162
+
163
+ # Create summary DataFrame
164
+ summary_data = {
165
+ "Metric": [
166
+ "Total Cooling Load",
167
+ "Sensible Cooling Load",
168
+ "Latent Cooling Load",
169
+ "Cooling Load per Area"
170
+ ],
171
+ "Value": [
172
+ results["cooling"]["total_load"],
173
+ results["cooling"]["sensible_load"],
174
+ results["cooling"]["latent_load"],
175
+ results["cooling"]["load_per_area"]
176
+ ],
177
+ "Unit": [
178
+ "kW",
179
+ "kW",
180
+ "kW",
181
+ "W/m²"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  ]
183
+ }
184
+
185
+ dataframes["Cooling Summary"] = pd.DataFrame(summary_data)
186
+
187
+ # Create component breakdown DataFrame
188
+ component_data = {
189
+ "Component": [
190
+ "Walls",
191
+ "Roof",
192
+ "Windows",
193
+ "Doors",
194
+ "People",
195
+ "Lighting",
196
+ "Equipment",
197
+ "Infiltration",
198
+ "Ventilation"
199
+ ],
200
+ "Load (kW)": [
201
+ results["cooling"]["component_loads"]["walls"],
202
+ results["cooling"]["component_loads"]["roof"],
203
+ results["cooling"]["component_loads"]["windows"],
204
+ results["cooling"]["component_loads"]["doors"],
205
+ results["cooling"]["component_loads"]["people"],
206
+ results["cooling"]["component_loads"]["lighting"],
207
+ results["cooling"]["component_loads"]["equipment"],
208
+ results["cooling"]["component_loads"]["infiltration"],
209
+ results["cooling"]["component_loads"]["ventilation"]
210
+ ],
211
+ "Percentage (%)": [
212
+ results["cooling"]["component_loads"]["walls"] / results["cooling"]["total_load"] * 100,
213
+ results["cooling"]["component_loads"]["roof"] / results["cooling"]["total_load"] * 100,
214
+ results["cooling"]["component_loads"]["windows"] / results["cooling"]["total_load"] * 100,
215
+ results["cooling"]["component_loads"]["doors"] / results["cooling"]["total_load"] * 100,
216
+ results["cooling"]["component_loads"]["people"] / results["cooling"]["total_load"] * 100,
217
+ results["cooling"]["component_loads"]["lighting"] / results["cooling"]["total_load"] * 100,
218
+ results["cooling"]["component_loads"]["equipment"] / results["cooling"]["total_load"] * 100,
219
+ results["cooling"]["component_loads"]["infiltration"] / results["cooling"]["total_load"] * 100,
220
+ results["cooling"]["component_loads"]["ventilation"] / results["cooling"]["total_load"] * 100
221
+ ]
222
+ }
223
+
224
+ dataframes["Cooling Components"] = pd.DataFrame(component_data)
225
+
226
+ # Create detailed loads DataFrames
227
+
228
+ # Walls
229
+ wall_data = []
230
+ for wall in results["cooling"]["detailed_loads"]["walls"]:
231
+ wall_data.append({
232
+ "Name": wall["name"],
233
+ "Orientation": wall["orientation"],
234
+ "Area (m²)": wall["area"],
235
+ "U-Value (W/m²·K)": wall["u_value"],
236
+ "CLTD (°C)": wall["cltd"],
237
+ "Load (kW)": wall["load"]
238
+ })
239
+
240
+ if wall_data:
241
+ dataframes["Cooling Walls"] = pd.DataFrame(wall_data)
242
+
243
+ # Roofs
244
+ roof_data = []
245
+ for roof in results["cooling"]["detailed_loads"]["roofs"]:
246
+ roof_data.append({
247
+ "Name": roof["name"],
248
+ "Orientation": roof["orientation"],
249
+ "Area (m²)": roof["area"],
250
+ "U-Value (W/m²·K)": roof["u_value"],
251
+ "CLTD (°C)": roof["cltd"],
252
+ "Load (kW)": roof["load"]
253
+ })
254
+
255
+ if roof_data:
256
+ dataframes["Cooling Roofs"] = pd.DataFrame(roof_data)
257
+
258
+ # Windows
259
+ window_data = []
260
+ for window in results["cooling"]["detailed_loads"]["windows"]:
261
+ window_data.append({
262
+ "Name": window["name"],
263
+ "Orientation": window["orientation"],
264
+ "Area (m²)": window["area"],
265
+ "U-Value (W/m²·K)": window["u_value"],
266
+ "SHGC": window["shgc"],
267
+ "SCL (W/m²)": window["scl"],
268
+ "Load (kW)": window["load"]
269
+ })
270
+
271
+ if window_data:
272
+ dataframes["Cooling Windows"] = pd.DataFrame(window_data)
273
+
274
+ # Doors
275
+ door_data = []
276
+ for door in results["cooling"]["detailed_loads"]["doors"]:
277
+ door_data.append({
278
+ "Name": door["name"],
279
+ "Orientation": door["orientation"],
280
+ "Area (m²)": door["area"],
281
+ "U-Value (W/m²·K)": door["u_value"],
282
+ "CLTD (°C)": door["cltd"],
283
+ "Load (kW)": door["load"]
284
+ })
285
+
286
+ if door_data:
287
+ dataframes["Cooling Doors"] = pd.DataFrame(door_data)
288
+
289
+ # Internal loads
290
+ internal_data = []
291
+ for internal_load in results["cooling"]["detailed_loads"]["internal"]:
292
+ internal_data.append({
293
+ "Type": internal_load["type"],
294
+ "Name": internal_load["name"],
295
+ "Quantity": internal_load["quantity"],
296
+ "Heat Gain (W)": internal_load["heat_gain"],
297
+ "CLF": internal_load["clf"],
298
+ "Load (kW)": internal_load["load"]
299
+ })
300
+
301
+ if internal_data:
302
+ dataframes["Cooling Internal Loads"] = pd.DataFrame(internal_data)
303
+
304
+ # Infiltration and ventilation
305
+ air_data = [
306
+ {
307
+ "Type": "Infiltration",
308
+ "Air Flow (m³/s)": results["cooling"]["detailed_loads"]["infiltration"]["air_flow"],
309
+ "Sensible Load (kW)": results["cooling"]["detailed_loads"]["infiltration"]["sensible_load"],
310
+ "Latent Load (kW)": results["cooling"]["detailed_loads"]["infiltration"]["latent_load"],
311
+ "Total Load (kW)": results["cooling"]["detailed_loads"]["infiltration"]["total_load"]
312
+ },
313
+ {
314
+ "Type": "Ventilation",
315
+ "Air Flow (m³/s)": results["cooling"]["detailed_loads"]["ventilation"]["air_flow"],
316
+ "Sensible Load (kW)": results["cooling"]["detailed_loads"]["ventilation"]["sensible_load"],
317
+ "Latent Load (kW)": results["cooling"]["detailed_loads"]["ventilation"]["latent_load"],
318
+ "Total Load (kW)": results["cooling"]["detailed_loads"]["ventilation"]["total_load"]
319
+ }
320
+ ]
321
+
322
+ dataframes["Cooling Air Exchange"] = pd.DataFrame(air_data)
323
+
324
+ return dataframes
325
 
326
  @staticmethod
327
  def create_heating_load_dataframes(results: Dict[str, Any]) -> Dict[str, pd.DataFrame]:
 
334
  Returns:
335
  Dictionary with DataFrames for Excel export
336
  """
337
+ dataframes = {}
338
+
339
+ # Create summary DataFrame
340
+ summary_data = {
341
+ "Metric": [
342
+ "Total Heating Load",
343
+ "Heating Load per Area",
344
+ "Design Heat Loss",
345
+ "Safety Factor"
346
+ ],
347
+ "Value": [
348
+ results["heating"]["total_load"],
349
+ results["heating"]["load_per_area"],
350
+ results["heating"]["design_heat_loss"],
351
+ results["heating"]["safety_factor"]
352
+ ],
353
+ "Unit": [
354
+ "kW",
355
+ "W/m²",
356
+ "kW",
357
+ "%"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  ]
359
+ }
360
+
361
+ dataframes["Heating Summary"] = pd.DataFrame(summary_data)
362
+
363
+ # Create component breakdown DataFrame
364
+ component_data = {
365
+ "Component": [
366
+ "Walls",
367
+ "Roof",
368
+ "Floor",
369
+ "Windows",
370
+ "Doors",
371
+ "Infiltration",
372
+ "Ventilation"
373
+ ],
374
+ "Load (kW)": [
375
+ results["heating"]["component_loads"]["walls"],
376
+ results["heating"]["component_loads"]["roof"],
377
+ results["heating"]["component_loads"]["floor"],
378
+ results["heating"]["component_loads"]["windows"],
379
+ results["heating"]["component_loads"]["doors"],
380
+ results["heating"]["component_loads"]["infiltration"],
381
+ results["heating"]["component_loads"]["ventilation"]
382
+ ],
383
+ "Percentage (%)": [
384
+ results["heating"]["component_loads"]["walls"] / results["heating"]["total_load"] * 100,
385
+ results["heating"]["component_loads"]["roof"] / results["heating"]["total_load"] * 100,
386
+ results["heating"]["component_loads"]["floor"] / results["heating"]["total_load"] * 100,
387
+ results["heating"]["component_loads"]["windows"] / results["heating"]["total_load"] * 100,
388
+ results["heating"]["component_loads"]["doors"] / results["heating"]["total_load"] * 100,
389
+ results["heating"]["component_loads"]["infiltration"] / results["heating"]["total_load"] * 100,
390
+ results["heating"]["component_loads"]["ventilation"] / results["heating"]["total_load"] * 100
391
+ ]
392
+ }
393
+
394
+ dataframes["Heating Components"] = pd.DataFrame(component_data)
395
+
396
+ # Create detailed loads DataFrames
397
+
398
+ # Walls
399
+ wall_data = []
400
+ for wall in results["heating"]["detailed_loads"]["walls"]:
401
+ wall_data.append({
402
+ "Name": wall["name"],
403
+ "Orientation": wall["orientation"],
404
+ "Area (m²)": wall["area"],
405
+ "U-Value (W/m²·K)": wall["u_value"],
406
+ "Temperature Difference (°C)": wall["delta_t"],
407
+ "Load (kW)": wall["load"]
408
+ })
409
+
410
+ if wall_data:
411
+ dataframes["Heating Walls"] = pd.DataFrame(wall_data)
412
+
413
+ # Roofs
414
+ roof_data = []
415
+ for roof in results["heating"]["detailed_loads"]["roofs"]:
416
+ roof_data.append({
417
+ "Name": roof["name"],
418
+ "Orientation": roof["orientation"],
419
+ "Area (m²)": roof["area"],
420
+ "U-Value (W/m²·K)": roof["u_value"],
421
+ "Temperature Difference (°C)": roof["delta_t"],
422
+ "Load (kW)": roof["load"]
423
+ })
424
+
425
+ if roof_data:
426
+ dataframes["Heating Roofs"] = pd.DataFrame(roof_data)
427
+
428
+ # Floors
429
+ floor_data = []
430
+ for floor in results["heating"]["detailed_loads"]["floors"]:
431
+ floor_data.append({
432
+ "Name": floor["name"],
433
+ "Area (m²)": floor["area"],
434
+ "U-Value (W/m²·K)": floor["u_value"],
435
+ "Temperature Difference (°C)": floor["delta_t"],
436
+ "Load (kW)": floor["load"]
437
+ })
438
+
439
+ if floor_data:
440
+ dataframes["Heating Floors"] = pd.DataFrame(floor_data)
441
+
442
+ # Windows
443
+ window_data = []
444
+ for window in results["heating"]["detailed_loads"]["windows"]:
445
+ window_data.append({
446
+ "Name": window["name"],
447
+ "Orientation": window["orientation"],
448
+ "Area (m²)": window["area"],
449
+ "U-Value (W/m²·K)": window["u_value"],
450
+ "Temperature Difference (°C)": window["delta_t"],
451
+ "Load (kW)": window["load"]
452
+ })
453
+
454
+ if window_data:
455
+ dataframes["Heating Windows"] = pd.DataFrame(window_data)
456
+
457
+ # Doors
458
+ door_data = []
459
+ for door in results["heating"]["detailed_loads"]["doors"]:
460
+ door_data.append({
461
+ "Name": door["name"],
462
+ "Orientation": door["orientation"],
463
+ "Area (m²)": door["area"],
464
+ "U-Value (W/m²·K)": door["u_value"],
465
+ "Temperature Difference (°C)": door["delta_t"],
466
+ "Load (kW)": door["load"]
467
+ })
468
+
469
+ if door_data:
470
+ dataframes["Heating Doors"] = pd.DataFrame(door_data)
471
+
472
+ # Infiltration and ventilation
473
+ air_data = [
474
+ {
475
+ "Type": "Infiltration",
476
+ "Air Flow (m³/s)": results["heating"]["detailed_loads"]["infiltration"]["air_flow"],
477
+ "Temperature Difference (°C)": results["heating"]["detailed_loads"]["infiltration"]["delta_t"],
478
+ "Load (kW)": results["heating"]["detailed_loads"]["infiltration"]["load"]
479
+ },
480
+ {
481
+ "Type": "Ventilation",
482
+ "Air Flow (m³/s)": results["heating"]["detailed_loads"]["ventilation"]["air_flow"],
483
+ "Temperature Difference (°C)": results["heating"]["detailed_loads"]["ventilation"]["delta_t"],
484
+ "Load (kW)": results["heating"]["detailed_loads"]["ventilation"]["load"]
485
+ }
486
+ ]
487
+
488
+ dataframes["Heating Air Exchange"] = pd.DataFrame(air_data)
489
+
490
+ return dataframes
491
 
492
  @staticmethod
493
  def display_export_interface(session_state: Dict[str, Any]) -> None:
 
497
  Args:
498
  session_state: Streamlit session state containing calculation results
499
  """
500
+ st.header("Export Results")
501
+
502
+ # Check if calculations have been performed
503
+ if "calculation_results" not in session_state or not session_state["calculation_results"]:
504
+ st.warning("No calculation results available. Please run calculations first.")
505
+ return
506
+
507
+ # Create tabs for different export options
508
+ tab1, tab2, tab3 = st.tabs(["CSV Export", "Excel Export", "Scenario Export"])
509
+
510
+ with tab1:
511
+ DataExport._display_csv_export(session_state)
512
+
513
+ with tab2:
514
+ DataExport._display_excel_export(session_state)
515
+
516
+ with tab3:
517
+ DataExport._display_scenario_export(session_state)
 
 
 
 
518
 
519
  @staticmethod
520
  def _display_csv_export(session_state: Dict[str, Any]) -> None:
 
524
  Args:
525
  session_state: Streamlit session state containing calculation results
526
  """
527
+ st.subheader("CSV Export")
528
+
529
+ # Get results
530
+ results = session_state["calculation_results"]
531
+
532
+ # Create tabs for cooling and heating loads
533
+ tab1, tab2 = st.tabs(["Cooling Load CSV", "Heating Load CSV"])
534
+
535
+ with tab1:
536
+ # Create cooling load DataFrames
537
+ cooling_dfs = DataExport.create_cooling_load_dataframes(results)
538
+
539
+ # Display and export each DataFrame
540
+ for sheet_name, df in cooling_dfs.items():
541
+ st.write(f"### {sheet_name}")
542
+ st.dataframe(df)
543
 
544
+ # Add download button
545
+ csv_data = DataExport.export_to_csv(df)
546
+ if csv_data:
547
+ filename = f"{sheet_name.replace(' ', '_').lower()}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
548
+ download_link = DataExport.get_download_link(csv_data, filename, f"Download {sheet_name} CSV")
549
+ st.markdown(download_link, unsafe_allow_html=True)
550
+
551
+ with tab2:
552
+ # Create heating load DataFrames
553
+ heating_dfs = DataExport.create_heating_load_dataframes(results)
554
+
555
+ # Display and export each DataFrame
556
+ for sheet_name, df in heating_dfs.items():
557
+ st.write(f"### {sheet_name}")
558
+ st.dataframe(df)
559
 
560
+ # Add download button
561
+ csv_data = DataExport.export_to_csv(df)
562
+ if csv_data:
563
+ filename = f"{sheet_name.replace(' ', '_').lower()}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
564
+ download_link = DataExport.get_download_link(csv_data, filename, f"Download {sheet_name} CSV")
565
+ st.markdown(download_link, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
566
 
567
  @staticmethod
568
  def _display_excel_export(session_state: Dict[str, Any]) -> None:
 
572
  Args:
573
  session_state: Streamlit session state containing calculation results
574
  """
575
+ st.subheader("Excel Export")
576
+
577
+ # Get results
578
+ results = session_state["calculation_results"]
579
+
580
+ # Create tabs for cooling, heating, and combined loads
581
+ tab1, tab2, tab3 = st.tabs(["Cooling Load Excel", "Heating Load Excel", "Combined Excel"])
582
+
583
+ with tab1:
584
+ # Create cooling load DataFrames
585
+ cooling_dfs = DataExport.create_cooling_load_dataframes(results)
586
+
587
+ # Add download button
588
+ excel_data = DataExport.export_to_excel(cooling_dfs)
589
+ if excel_data:
590
+ filename = f"cooling_load_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
591
+ download_link = DataExport.get_download_link(
592
+ excel_data,
593
+ filename,
594
+ "Download Cooling Load Excel",
595
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
596
+ )
597
+ st.markdown(download_link, unsafe_allow_html=True)
598
+
599
+ # Display preview
600
+ st.write("### Excel Preview")
601
+ st.write("The Excel file will contain the following sheets:")
602
+ for sheet_name in cooling_dfs.keys():
603
+ st.write(f"- {sheet_name}")
604
+
605
+ with tab2:
606
+ # Create heating load DataFrames
607
+ heating_dfs = DataExport.create_heating_load_dataframes(results)
608
+
609
+ # Add download button
610
+ excel_data = DataExport.export_to_excel(heating_dfs)
611
+ if excel_data:
612
+ filename = f"heating_load_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
613
+ download_link = DataExport.get_download_link(
614
+ excel_data,
615
+ filename,
616
+ "Download Heating Load Excel",
617
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
618
+ )
619
+ st.markdown(download_link, unsafe_allow_html=True)
620
+
621
+ # Display preview
622
+ st.write("### Excel Preview")
623
+ st.write("The Excel file will contain the following sheets:")
624
+ for sheet_name in heating_dfs.keys():
625
+ st.write(f"- {sheet_name}")
626
+
627
+ with tab3:
628
+ # Create combined DataFrames
629
+ combined_dfs = {}
630
+
631
+ # Add project information
632
+ if "building_info" in session_state:
633
+ project_info = [
634
+ {"Parameter": "Project Name", "Value": session_state["building_info"].get("project_name", "")},
635
+ {"Parameter": "Building Name", "Value": session_state["building_info"].get("building_name", "")},
636
+ {"Parameter": "Location", "Value": session_state["building_info"].get("location", "")},
637
+ {"Parameter": "Climate Zone", "Value": session_state["building_info"].get("climate_zone", "")},
638
+ {"Parameter": "Building Type", "Value": session_state["building_info"].get("building_type", "")},
639
+ {"Parameter": "Floor Area", "Value": session_state["building_info"].get("floor_area", "")},
640
+ {"Parameter": "Number of Floors", "Value": session_state["building_info"].get("num_floors", "")},
641
+ {"Parameter": "Floor Height", "Value": session_state["building_info"].get("floor_height", "")},
642
+ {"Parameter": "Orientation", "Value": session_state["building_info"].get("orientation", "")},
643
+ {"Parameter": "Occupancy", "Value": session_state["building_info"].get("occupancy", "")},
644
+ {"Parameter": "Operating Hours", "Value": session_state["building_info"].get("operating_hours", "")},
645
+ {"Parameter": "Date", "Value": datetime.now().strftime("%Y-%m-%d")},
646
+ {"Parameter": "Time", "Value": datetime.now().strftime("%H:%M:%S")}
647
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
648
 
649
+ combined_dfs["Project Information"] = pd.DataFrame(project_info)
650
+
651
+ # Add cooling load DataFrames
652
+ cooling_dfs = DataExport.create_cooling_load_dataframes(results)
653
+ for sheet_name, df in cooling_dfs.items():
654
+ combined_dfs[sheet_name] = df
655
+
656
+ # Add heating load DataFrames
657
+ heating_dfs = DataExport.create_heating_load_dataframes(results)
658
+ for sheet_name, df in heating_dfs.items():
659
+ combined_dfs[sheet_name] = df
660
+
661
+ # Add download button
662
+ excel_data = DataExport.export_to_excel(combined_dfs)
663
+ if excel_data:
664
+ filename = f"hvac_load_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
665
+ download_link = DataExport.get_download_link(
666
+ excel_data,
667
+ filename,
668
+ "Download Combined Excel Report",
669
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
670
+ )
671
+ st.markdown(download_link, unsafe_allow_html=True)
672
+
673
+ # Display preview
674
+ st.write("### Excel Preview")
675
+ st.write("The Excel file will contain the following sheets:")
676
+ for sheet_name in combined_dfs.keys():
677
+ st.write(f"- {sheet_name}")
678
 
679
  @staticmethod
680
  def _display_scenario_export(session_state: Dict[str, Any]) -> None:
 
684
  Args:
685
  session_state: Streamlit session state containing calculation results
686
  """
687
+ st.subheader("Scenario Export")
688
+
689
+ # Check if there are saved scenarios
690
+ if "saved_scenarios" not in session_state or not session_state["saved_scenarios"]:
691
+ st.info("No saved scenarios available for export. Save the current results as a scenario to enable export.")
692
 
693
+ # Add button to save current results as a scenario
694
+ scenario_name = st.text_input("Scenario Name", value="Baseline")
695
+
696
+ if st.button("Save Current Results as Scenario"):
697
+ if "saved_scenarios" not in session_state:
698
+ session_state["saved_scenarios"] = {}
699
 
700
+ # Save current results as a scenario
701
+ session_state["saved_scenarios"][scenario_name] = {
702
+ "results": session_state["calculation_results"],
703
+ "building_info": session_state["building_info"],
704
+ "components": session_state["components"],
705
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
706
+ }
 
 
 
 
 
 
 
 
 
 
707
 
708
+ st.success(f"Scenario '{scenario_name}' saved successfully!")
709
+ st.experimental_rerun()
710
+ else:
711
+ # Display saved scenarios
712
+ st.write("### Saved Scenarios")
713
+
714
+ # Create selectbox for scenarios
715
+ scenario_names = list(session_state["saved_scenarios"].keys())
716
+ selected_scenario = st.selectbox("Select Scenario to Export", scenario_names)
717
+
718
+ if selected_scenario:
719
+ # Get selected scenario
720
+ scenario = session_state["saved_scenarios"][selected_scenario]
721
 
722
+ # Display scenario information
723
+ st.write(f"**Scenario:** {selected_scenario}")
724
+ st.write(f"**Timestamp:** {scenario['timestamp']}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
725
 
726
+ # Add download button
727
+ json_data = DataExport.export_scenario_to_json(scenario)
728
+ if json_data:
729
+ filename = f"{selected_scenario.replace(' ', '_').lower()}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
730
  download_link = DataExport.get_download_link(
731
+ json_data,
732
  filename,
733
+ "Download Scenario JSON",
734
+ "application/json"
735
  )
736
  st.markdown(download_link, unsafe_allow_html=True)
737
+
738
+ # Add button to export all scenarios
739
+ if st.button("Export All Scenarios"):
740
+ # Create a zip file in memory
741
+ import zipfile
742
+ from io import BytesIO
743
+
744
+ zip_buffer = BytesIO()
745
+ with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
746
+ for scenario_name, scenario in session_state["saved_scenarios"].items():
747
+ # Export scenario to JSON
748
+ json_data = DataExport.export_scenario_to_json(scenario)
749
+ if json_data:
750
+ filename = f"{scenario_name.replace(' ', '_').lower()}.json"
751
+ zip_file.writestr(filename, json_data)
752
+
753
+ # Add download button for zip file
754
+ zip_buffer.seek(0)
755
+ zip_data = zip_buffer.getvalue()
756
+
757
+ filename = f"all_scenarios_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
758
+ download_link = DataExport.get_download_link(
759
+ zip_data,
760
+ filename,
761
+ "Download All Scenarios (ZIP)",
762
+ "application/zip"
763
+ )
764
+ st.markdown(download_link, unsafe_allow_html=True)
765
+
766
 
767
+ # Create a singleton instance
768
+ data_export = DataExport()
769
 
770
+ # Example usage
771
  if __name__ == "__main__":
772
  import streamlit as st
773
 
 
866
  }
867
  }
868
 
869
+ # Display export interface
870
+ data_export.display_export_interface(st.session_state)
 
app/data_persistence.py CHANGED
@@ -41,7 +41,6 @@ class DataPersistence:
41
  "internal_loads": session_state.get("internal_loads", {}),
42
  "calculation_settings": session_state.get("calculation_settings", {}),
43
  "saved_scenarios": DataPersistence._serialize_scenarios(session_state.get("saved_scenarios", {})),
44
- "climate_data": session_state.get("climate_data", {}),
45
  "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
46
  }
47
 
@@ -83,21 +82,6 @@ class DataPersistence:
83
  if json_data:
84
  project_data = json.loads(json_data)
85
 
86
- # Validate climate data latitude
87
- if "climate_data" in project_data:
88
- valid_latitudes = [24, 32, 40, 48, 56]
89
- for location_id, location in project_data["climate_data"].items():
90
- if "latitude" in location:
91
- try:
92
- input_latitude = float(location["latitude"])
93
- nearest_latitude = min(valid_latitudes, key=lambda x: abs(x - input_latitude))
94
- if abs(input_latitude - nearest_latitude) > 0.1: # Allow small float differences
95
- st.warning(f"Latitude {input_latitude} for {location_id} is invalid for ASHRAE tables. Using nearest valid latitude: {nearest_latitude}.")
96
- location["latitude"] = nearest_latitude
97
- except ValueError:
98
- st.error(f"Invalid latitude format for {location_id}: {location['latitude']}.")
99
- return None
100
-
101
  # Deserialize components
102
  if "components" in project_data:
103
  project_data["components"] = DataPersistence._deserialize_components(project_data["components"])
@@ -553,4 +537,4 @@ if __name__ == "__main__":
553
  }
554
 
555
  # Display project management interface
556
- data_persistence.display_project_management(st.session_state)
 
41
  "internal_loads": session_state.get("internal_loads", {}),
42
  "calculation_settings": session_state.get("calculation_settings", {}),
43
  "saved_scenarios": DataPersistence._serialize_scenarios(session_state.get("saved_scenarios", {})),
 
44
  "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
45
  }
46
 
 
82
  if json_data:
83
  project_data = json.loads(json_data)
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  # Deserialize components
86
  if "components" in project_data:
87
  project_data["components"] = DataPersistence._deserialize_components(project_data["components"])
 
537
  }
538
 
539
  # Display project management interface
540
+ data_persistence.display_project_management(st.session_state)
app/main.py CHANGED
@@ -11,129 +11,111 @@ import json
11
  import pycountry
12
  import os
13
  import sys
14
- import logging
15
  from typing import Dict, List, Any, Optional, Tuple
16
- from uuid import uuid4
17
-
18
- # Configure logging
19
- logging.basicConfig(level=logging.INFO)
20
- logger = logging.getLogger(__name__)
21
 
22
  # Import application modules
23
- try:
24
- from app.building_info_form import BuildingInfoForm
25
- from app.component_selection import ComponentSelectionInterface, Orientation, ComponentType, Wall, Roof, Floor, Window, Door
26
- from app.results_display import ResultsDisplay
27
- from app.data_validation import DataValidation
28
- from app.data_persistence import DataPersistence
29
- from app.data_export import DataExport
30
- except ImportError as e:
31
- logger.error(f"Error importing application modules: {str(e)}")
32
- raise
33
 
34
  # Import data modules
35
- try:
36
- from data.reference_data import ReferenceData
37
- from data.climate_data import ClimateData, ClimateLocation
38
- from data.ashrae_tables import ASHRAETables
39
- from data.building_components import Wall as WallModel, Roof as RoofModel
40
- except ImportError as e:
41
- logger.error(f"Error importing data modules: {str(e)}")
42
- raise
43
 
44
  # Import utility modules
45
- try:
46
- from utils.u_value_calculator import UValueCalculator
47
- from utils.shading_system import ShadingSystem
48
- from utils.area_calculation_system import AreaCalculationSystem
49
- from utils.psychrometrics import Psychrometrics
50
- from utils.heat_transfer import HeatTransferCalculations
51
- from utils.cooling_load import CoolingLoadCalculator
52
- from utils.heating_load import HeatingLoadCalculator
53
- from utils.component_visualization import ComponentVisualization
54
- from utils.scenario_comparison import ScenarioComparisonVisualization
55
- from utils.psychrometric_visualization import PsychrometricVisualization
56
- from utils.time_based_visualization import TimeBasedVisualization
57
- except ImportError as e:
58
- logger.error(f"Error importing utility modules: {str(e)}")
59
- raise
60
-
61
 
62
  class HVACCalculator:
63
  def __init__(self):
64
- try:
65
- st.set_page_config(
66
- page_title="HVAC Load Calculator",
67
- page_icon="🌡️",
68
- layout="wide",
69
- initial_sidebar_state="expanded"
70
- )
71
-
72
- # Initialize session state
73
- self._initialize_session_state()
74
-
75
- # Initialize modules
76
- self.building_info_form = BuildingInfoForm()
77
- self.component_selection = ComponentSelectionInterface()
78
- self.results_display = ResultsDisplay()
79
- self.data_validation = DataValidation()
80
- self.data_persistence = DataPersistence()
81
- self.data_export = DataExport()
82
- self.cooling_calculator = CoolingLoadCalculator()
83
- self.heating_calculator = HeatingLoadCalculator()
84
- self.reference_data = ReferenceData()
85
-
86
- # Validate DataExport has display method
87
- if not hasattr(self.data_export, 'display'):
88
- logger.error("DataExport object missing 'display' method")
89
- raise AttributeError("DataExport object missing 'display' method")
90
-
91
- # Initialize climate data
92
- self._initialize_climate_data()
93
-
94
- self.setup_layout()
95
- except Exception as e:
96
- logger.error(f"Error initializing HVACCalculator: {str(e)}")
97
- st.error(f"Failed to initialize application: {str(e)}")
98
- raise
99
-
100
- def _initialize_session_state(self):
101
- """Initialize session state variables."""
102
- defaults = {
103
- 'page': 'Building Information',
104
- 'building_info': {"project_name": ""},
105
- 'components': {'walls': [], 'roofs': [], 'floors': [], 'windows': [], 'doors': []},
106
- 'internal_loads': {'people': [], 'lighting': [], 'equipment': []},
107
- 'calculation_results': {'cooling': {}, 'heating': {}},
108
- 'saved_scenarios': {},
109
- 'climate_data': {},
110
- 'debug_mode': False
111
- }
112
- for key, value in defaults.items():
113
- if key not in st.session_state:
114
- st.session_state[key] = value
115
-
116
- def _initialize_climate_data(self):
117
- """Initialize climate data from JSON or default."""
 
 
118
  if 'climate_data_obj' not in st.session_state:
119
  st.session_state.climate_data_obj = ClimateData()
120
  self.climate_data = st.session_state.climate_data_obj
121
-
 
122
  try:
123
  if not self.climate_data.locations:
124
  self.climate_data = ClimateData.from_json("/home/user/app/climate_data.json")
125
  st.session_state.climate_data_obj = self.climate_data
126
  except FileNotFoundError:
127
  st.warning("Default climate data file not found. Please enter climate data manually.")
128
- except Exception as e:
129
- logger.error(f"Error loading climate data: {str(e)}")
130
- st.error("Failed to initialize climate data.")
131
-
132
  def setup_layout(self):
133
- """Setup the application layout."""
134
  st.sidebar.title("HVAC Load Calculator")
135
  st.sidebar.markdown("---")
136
-
137
  st.sidebar.subheader("Navigation")
138
  pages = [
139
  "Building Information",
@@ -143,294 +125,328 @@ class HVACCalculator:
143
  "Calculation Results",
144
  "Export Data"
145
  ]
146
-
147
  selected_page = st.sidebar.radio("Go to", pages, index=pages.index(st.session_state.page))
 
148
  if selected_page != st.session_state.page:
149
  st.session_state.page = selected_page
150
-
151
  self.display_page(st.session_state.page)
152
-
153
  st.sidebar.markdown("---")
154
  st.sidebar.info(
155
- "HVAC Load Calculator v1.0.2\n\n"
156
  "Based on ASHRAE steady-state calculation methods\n\n"
157
  "Developed by: Dr Majed Abuseif\n\n"
158
  "School of Architecture and Built Environment\n\n"
159
  "Deakin University\n\n"
160
  "© 2025"
161
  )
162
-
163
  def display_page(self, page: str):
164
- """Display the selected page."""
165
- page_functions = {
166
- "Building Information": self.building_info_form.display_building_info_form,
167
- "Climate Data": self.climate_data.display_climate_input,
168
- "Building Components": self.component_selection.display_component_selection,
169
- "Internal Loads": self.display_internal_loads,
170
- "Calculation Results": self.display_calculation_results,
171
- "Export Data": lambda state: self._display_export_data(state)
172
- }
173
- try:
174
- page_functions[page](st.session_state)
175
- except AttributeError as e:
176
- logger.error(f"Error displaying page {page}: {str(e)}")
177
- st.error(f"Error displaying {page} page: {str(e)}")
178
- except Exception as e:
179
- logger.error(f"Unexpected error displaying page {page}: {str(e)}")
180
- st.error(f"Unexpected error displaying {page} page: {str(e)}")
181
-
182
- def _display_export_data(self, state):
183
- """Wrapper for DataExport.display with error handling."""
184
- try:
185
- self.data_export.display(state)
186
- except AttributeError as e:
187
- logger.error(f"DataExport.display failed: {str(e)}")
188
- st.error("Export Data functionality is unavailable. Please check DataExport implementation.")
189
- st.write("Placeholder for Export Data page. Select export options below:")
190
- st.button("Export to CSV (Not Implemented)")
191
- st.button("Export to JSON (Not Implemented)")
192
-
193
  def generate_climate_id(self, country: str, city: str) -> str:
194
  """Generate a climate ID from country and city names."""
195
  try:
196
  country = country.strip().title()
197
  city = city.strip().title()
198
  if len(country) < 2 or len(city) < 3:
199
- raise ValueError("Country and city names must be at least 2 and 3 characters long.")
200
- country_code = pycountry.countries.search_fuzzy(country)[0].alpha_2
201
- return f"{country_code}-{city[:3].upper()}"
202
  except Exception as e:
203
- logger.error(f"Error generating climate ID: {str(e)}")
204
  raise ValueError(f"Invalid country or city name: {str(e)}")
205
-
206
  def validate_calculation_inputs(self) -> Tuple[bool, str]:
207
  """Validate inputs for cooling and heating calculations."""
208
- try:
209
- building_info = st.session_state.get('building_info', {})
210
- components = st.session_state.get('components', {})
211
- climate_data = st.session_state.get('climate_data', {})
212
-
213
- if not building_info.get('floor_area', 0) > 0:
214
- return False, "Floor area must be positive."
215
- if not any(components.get(key, []) for key in ['walls', 'roofs', 'windows']):
216
- return False, "At least one wall, roof, or window must be defined."
217
- if not climate_data:
218
- return False, "Climate data is missing."
219
-
220
- for component_type in ['walls', 'roofs', 'windows', 'doors', 'floors']:
221
- for comp in components.get(component_type, []):
222
- if comp.area <= 0:
223
- return False, f"Invalid area for {component_type}: {comp.name}"
224
- if comp.u_value <= 0:
225
- return False, f"Invalid U-value for {component_type}: {comp.name}"
226
-
227
- return True, "Inputs valid."
228
- except Exception as e:
229
- logger.error(f"Error validating inputs: {str(e)}")
230
- return False, f"Validation error: {str(e)}"
231
-
232
  def validate_internal_load(self, load_type: str, new_load: Dict) -> Tuple[bool, str]:
233
  """Validate if a new internal load is unique and within limits."""
234
- try:
235
- loads = st.session_state.internal_loads.get(load_type, [])
236
- max_loads = 50
237
-
238
- if len(loads) >= max_loads:
239
- return False, f"Maximum of {max_loads} {load_type} loads reached."
240
-
241
- for existing_load in loads:
242
- if load_type == 'people':
243
- if all(existing_load.get(k) == new_load.get(k) for k in ['name', 'num_people', 'activity_level', 'zone_type', 'hours_in_operation']):
244
- return False, f"Duplicate people load '{new_load['name']}' already exists."
245
- elif load_type == 'lighting':
246
- if all(existing_load.get(k) == new_load.get(k) for k in ['name', 'type', 'power', 'usage_factor', 'zone_type', 'hours_in_operation']):
247
- return False, f"Duplicate lighting load '{new_load['name']}' already exists."
248
- elif load_type == 'equipment':
249
- if all(existing_load.get(k) == new_load.get(k) for k in ['name', 'type', 'power', 'usage_factor', 'radiation_fraction', 'zone_type', 'hours_in_operation']):
250
- return False, f"Duplicate equipment load '{new_load['name']}' already exists."
251
-
252
- return True, "Valid load."
253
- except Exception as e:
254
- logger.error(f"Error validating internal load: {str(e)}")
255
- return False, f"Validation error: {str(e)}"
256
-
 
 
 
 
 
 
 
 
 
 
257
  def display_internal_loads(self):
258
- """Display internal loads interface."""
259
  st.title("Internal Loads")
260
-
 
261
  if st.button("Reset All Internal Loads"):
262
  st.session_state.internal_loads = {'people': [], 'lighting': [], 'equipment': []}
263
  st.success("All internal loads reset!")
264
  st.rerun()
265
-
266
  tabs = st.tabs(["People", "Lighting", "Equipment"])
267
-
268
  with tabs[0]:
269
- self._display_people_loads()
270
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  with tabs[1]:
272
- self._display_lighting_loads()
273
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  with tabs[2]:
275
- self._display_equipment_loads()
276
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  col1, col2 = st.columns(2)
278
  with col1:
279
- st.button("Back to Building Components", on_click=lambda: setattr(st.session_state, "page", "Building Components"))
 
 
 
280
  with col2:
281
- st.button("Continue to Calculation Results", on_click=lambda: setattr(st.session_state, "page", "Calculation Results"))
282
-
283
- def _display_people_loads(self):
284
- """Display people loads form and table."""
285
- st.subheader("People")
286
- activity_levels = list(self.reference_data.internal_loads.get('occupancy', {}).keys())
287
-
288
- with st.form("people_form"):
289
- num_people = st.number_input("Number of People", min_value=0, value=0, step=1)
290
- activity_level = st.selectbox("Activity Level", activity_levels if activity_levels else ["office_typing", "retail_sales"])
291
- zone_type = st.selectbox("Zone Type", ["Office", "Classroom", "Retail", "Residential"])
292
- hours_in_operation = st.number_input("Hours in Operation", min_value=0.0, max_value=24.0, value=8.0, step=0.5)
293
- people_name = st.text_input("Name", value="Occupants")
294
-
295
- if st.form_submit_button("Add People Load"):
296
- people_load = {
297
- "id": f"people_{str(uuid4())}",
298
- "name": people_name,
299
- "num_people": num_people,
300
- "activity_level": activity_level,
301
- "zone_type": zone_type,
302
- "hours_in_operation": hours_in_operation
303
- }
304
- is_valid, message = self.validate_internal_load('people', people_load)
305
- if is_valid:
306
- st.session_state.internal_loads['people'].append(people_load)
307
- st.success("People load added!")
308
- st.rerun()
309
- else:
310
- st.error(message)
311
-
312
- if st.session_state.internal_loads['people']:
313
- people_df = pd.DataFrame(st.session_state.internal_loads['people'])
314
- st.dataframe(people_df, use_container_width=True)
315
-
316
- selected_people = st.multiselect("Select People Loads to Delete", [load['id'] for load in st.session_state.internal_loads['people']])
317
- if st.button("Delete Selected People Loads"):
318
- st.session_state.internal_loads['people'] = [load for load in st.session_state.internal_loads['people'] if load['id'] not in selected_people]
319
- st.success("Selected people loads deleted!")
320
- st.rerun()
321
-
322
- def _display_lighting_loads(self):
323
- """Display lighting loads form and table."""
324
- st.subheader("Lighting")
325
- lighting_types = list(self.reference_data.internal_loads.get('lighting', {}).keys())
326
-
327
- with st.form("lighting_form"):
328
- lighting_type = st.selectbox("Lighting Type", lighting_types if lighting_types else ["led_high_efficiency"])
329
- power = st.number_input("Power (W)", min_value=0.0, value=1000.0, step=100.0)
330
- usage_factor = st.number_input("Usage Factor", min_value=0.0, max_value=1.0, value=0.8, step=0.1)
331
- zone_type = st.selectbox("Zone Type", ["Office", "Classroom", "Retail", "Residential"])
332
- hours_in_operation = st.number_input("Hours in Operation", min_value=0.0, max_value=24.0, value=8.0, step=0.5)
333
- lighting_name = st.text_input("Name", value="General Lighting")
334
-
335
- if st.form_submit_button("Add Lighting Load"):
336
- lighting_load = {
337
- "id": f"lighting_{str(uuid4())}",
338
- "name": lighting_name,
339
- "type": lighting_type,
340
- "power": power,
341
- "usage_factor": usage_factor,
342
- "zone_type": zone_type,
343
- "hours_in_operation": hours_in_operation
344
- }
345
- is_valid, message = self.validate_internal_load('lighting', lighting_load)
346
- if is_valid:
347
- st.session_state.internal_loads['lighting'].append(lighting_load)
348
- st.success("Lighting load added!")
349
- st.rerun()
350
- else:
351
- st.error(message)
352
-
353
- if st.session_state.internal_loads['lighting']:
354
- lighting_df = pd.DataFrame(st.session_state.internal_loads['lighting'])
355
- st.dataframe(lighting_df, use_container_width=True)
356
-
357
- selected_lighting = st.multiselect("Select Lighting Loads to Delete", [load['id'] for load in st.session_state.internal_loads['lighting']])
358
- if st.button("Delete Selected Lighting Loads"):
359
- st.session_state.internal_loads['lighting'] = [load for load in st.session_state.internal_loads['lighting'] if load['id'] not in selected_lighting]
360
- st.success("Selected lighting loads deleted!")
361
- st.rerun()
362
-
363
- def _display_equipment_loads(self):
364
- """Display equipment loads form and table."""
365
- st.subheader("Equipment")
366
- equipment_types = list(self.reference_data.internal_loads.get('equipment', {}).keys())
367
-
368
- with st.form("equipment_form"):
369
- equipment_type = st.selectbox("Equipment Type", equipment_types if equipment_types else ["computer_workstation"])
370
- power = st.number_input("Power (W)", min_value=0.0, value=500.0, step=100.0)
371
- usage_factor = st.number_input("Usage Factor", min_value=0.0, max_value=1.0, value=0.7, step=0.1)
372
- radiation_fraction = st.number_input("Radiation Fraction", min_value=0.0, max_value=1.0, value=0.3, step=0.1)
373
- zone_type = st.selectbox("Zone Type", ["Office", "Classroom", "Retail", "Residential"])
374
- hours_in_operation = st.number_input("Hours in Operation", min_value=0.0, max_value=24.0, value=8.0, step=0.5)
375
- equipment_name = st.text_input("Name", value="Office Equipment")
376
-
377
- if st.form_submit_button("Add Equipment Load"):
378
- equipment_load = {
379
- "id": f"equipment_{str(uuid4())}",
380
- "name": equipment_name,
381
- "type": equipment_type,
382
- "power": power,
383
- "usage_factor": usage_factor,
384
- "radiation_fraction": radiation_fraction,
385
- "zone_type": zone_type,
386
- "hours_in_operation": hours_in_operation
387
- }
388
- is_valid, message = self.validate_internal_load('equipment', equipment_load)
389
- if is_valid:
390
- st.session_state.internal_loads['equipment'].append(equipment_load)
391
- st.success("Equipment load added!")
392
- st.rerun()
393
- else:
394
- st.error(message)
395
-
396
- if st.session_state.internal_loads['equipment']:
397
- equipment_df = pd.DataFrame(st.session_state.internal_loads['equipment'])
398
- st.dataframe(equipment_df, use_container_width=True)
399
-
400
- selected_equipment = st.multiselect("Select Equipment Loads to Delete", [load['id'] for load in st.session_state.internal_loads['equipment']])
401
- if st.button("Delete Selected Equipment Loads"):
402
- st.session_state.internal_loads['equipment'] = [load for load in st.session_state.internal_loads['equipment'] if load['id'] not in selected_equipment]
403
- st.success("Selected equipment loads deleted!")
404
- st.rerun()
405
-
406
  def calculate_cooling(self) -> Tuple[bool, str, Dict]:
407
  """
408
  Calculate cooling loads using CoolingLoadCalculator.
409
  Returns: (success, message, results)
410
  """
411
  try:
 
412
  valid, message = self.validate_calculation_inputs()
413
  if not valid:
414
  return False, message, {}
415
-
 
416
  building_components = st.session_state.get('components', {})
417
  internal_loads = st.session_state.get('internal_loads', {})
418
  building_info = st.session_state.get('building_info', {})
419
-
 
 
 
 
 
420
  country = building_info.get('country', '').strip().title()
421
  city = building_info.get('city', '').strip().title()
422
  if not country or not city:
423
  return False, "Country and city must be set in Building Information.", {}
424
-
425
  climate_id = self.generate_climate_id(country, city)
426
  location = self.climate_data.get_location_by_id(climate_id, st.session_state)
427
  if not location:
428
  available_locations = list(self.climate_data.locations.keys())[:5]
429
  return False, f"No climate data for {climate_id}. Available locations: {', '.join(available_locations)}...", {}
430
-
 
431
  if not all(k in location for k in ['summer_design_temp_db', 'summer_design_temp_wb', 'monthly_temps', 'latitude']):
432
  return False, f"Invalid climate data for {climate_id}. Missing required fields.", {}
433
-
 
434
  outdoor_conditions = {
435
  'temperature': location['summer_design_temp_db'],
436
  'relative_humidity': location['monthly_humidity'].get('Jul', 50.0),
@@ -438,26 +454,46 @@ class HVACCalculator:
438
  'month': 'Jul',
439
  'latitude': f"{location['latitude']}N" if location['latitude'] >= 0 else f"{abs(location['latitude'])}S",
440
  'wind_speed': building_info.get('wind_speed', 4.0),
441
- 'day_of_year': 204
442
  }
443
  indoor_conditions = {
444
  'temperature': building_info.get('indoor_temp', 24.0),
445
  'relative_humidity': building_info.get('indoor_rh', 50.0)
446
  }
447
-
448
  if st.session_state.get('debug_mode', False):
449
  st.write("Debug: Cooling Input State", {
450
  'climate_id': climate_id,
451
  'outdoor_conditions': outdoor_conditions,
452
  'indoor_conditions': indoor_conditions,
453
  'components': {k: len(v) for k, v in building_components.items()},
454
- 'internal_loads': {k: len(v) for k, v in internal_loads.items()}
 
 
 
 
 
455
  })
456
-
 
457
  formatted_internal_loads = {
458
- 'people': [],
459
- 'lights': [],
460
- 'equipment': [],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
  'infiltration': {
462
  'flow_rate': building_info.get('infiltration_rate', 0.05),
463
  'height': building_info.get('building_height', 3.0),
@@ -468,36 +504,8 @@ class HVACCalculator:
468
  },
469
  'operating_hours': building_info.get('operating_hours', '8:00-18:00')
470
  }
471
-
472
- for load in internal_loads.get('people', []):
473
- ref_load = self.reference_data.get_internal_load('occupancy', load['activity_level'])
474
- formatted_internal_loads['people'].append({
475
- 'number': load['num_people'],
476
- 'sensible_heat': ref_load.get('sensible_heat', 70) if ref_load else 70,
477
- 'latent_heat': ref_load.get('latent_heat', 45) if ref_load else 45,
478
- 'hours_operation': f"{load['hours_in_operation']}:00-{load['hours_in_operation']+10}:00"
479
- })
480
-
481
- for load in internal_loads.get('lighting', []):
482
- ref_load = self.reference_data.get_internal_load('lighting', load['type'])
483
- formatted_internal_loads['lights'].append({
484
- 'power': load['power'],
485
- 'use_factor': load['usage_factor'],
486
- 'heat_to_space': ref_load.get('heat_to_space', 0.9) if ref_load else 0.9,
487
- 'hours_operation': f"{load['hours_in_operation']}h"
488
- })
489
-
490
- for load in internal_loads.get('equipment', []):
491
- ref_load = self.reference_data.get_internal_load('equipment', load['type'])
492
- formatted_internal_loads['equipment'].append({
493
- 'power': load['power'],
494
- 'use_factor': load['usage_factor'],
495
- 'radiation_factor': load['radiation_fraction'],
496
- 'sensible_fraction': ref_load.get('sensible_fraction', 0.9) if ref_load else 0.9,
497
- 'latent_fraction': ref_load.get('latent_fraction', 0.1) if ref_load else 0.1,
498
- 'hours_operation': f"{load['hours_in_operation']}h"
499
- })
500
-
501
  hourly_loads = self.cooling_calculator.calculate_hourly_cooling_loads(
502
  building_components=building_components,
503
  outdoor_conditions=outdoor_conditions,
@@ -506,16 +514,19 @@ class HVACCalculator:
506
  building_volume=building_info.get('floor_area', 100.0) * building_info.get('building_height', 3.0)
507
  )
508
  if not hourly_loads:
509
- return False, "Cooling hourly loads calculation failed.", {}
510
-
 
511
  design_loads = self.cooling_calculator.calculate_design_cooling_load(hourly_loads)
512
  if not design_loads:
513
- return False, "Cooling design loads calculation failed.", {}
514
-
 
515
  summary = self.cooling_calculator.calculate_cooling_load_summary(design_loads)
516
  if not summary:
517
- return False, "Cooling load summary calculation failed.", {}
518
-
 
519
  floor_area = building_info.get('floor_area', 100.0) or 100.0
520
  results = {
521
  'total_load': summary['total'] / 1000, # kW
@@ -554,7 +565,7 @@ class HVACCalculator:
554
  },
555
  'building_info': building_info
556
  }
557
-
558
  # Populate detailed loads
559
  for wall in building_components.get('walls', []):
560
  load = self.cooling_calculator.calculate_wall_cooling_load(
@@ -582,7 +593,7 @@ class HVACCalculator:
582
  ),
583
  'load': load / 1000
584
  })
585
-
586
  for roof in building_components.get('roofs', []):
587
  load = self.cooling_calculator.calculate_roof_cooling_load(
588
  roof=roof,
@@ -608,7 +619,7 @@ class HVACCalculator:
608
  ),
609
  'load': load / 1000
610
  })
611
-
612
  for window in building_components.get('windows', []):
613
  load_dict = self.cooling_calculator.calculate_window_cooling_load(
614
  window=window,
@@ -636,7 +647,7 @@ class HVACCalculator:
636
  ),
637
  'load': load_dict['total'] / 1000
638
  })
639
-
640
  for door in building_components.get('doors', []):
641
  load = self.cooling_calculator.calculate_door_cooling_load(
642
  door=door,
@@ -651,18 +662,16 @@ class HVACCalculator:
651
  'cltd': outdoor_conditions['temperature'] - indoor_conditions['temperature'],
652
  'load': load / 1000
653
  })
654
-
655
  for load_type, key in [('people', 'people'), ('lighting', 'lights'), ('equipment', 'equipment')]:
656
  for load in internal_loads.get(key, []):
657
  if load_type == 'people':
658
- ref_load = self.reference_data.get_internal_load('occupancy', load['activity_level'])
659
  load_dict = self.cooling_calculator.calculate_people_cooling_load(
660
  num_people=load['num_people'],
661
  activity_level=load['activity_level'],
662
  hour=design_loads['design_hour']
663
  )
664
  elif load_type == 'lighting':
665
- ref_load = self.reference_data.get_internal_load('lighting', load['type'])
666
  load_dict = {'total': self.cooling_calculator.calculate_lights_cooling_load(
667
  power=load['power'],
668
  use_factor=load['usage_factor'],
@@ -670,7 +679,6 @@ class HVACCalculator:
670
  hour=design_loads['design_hour']
671
  )}
672
  else:
673
- ref_load = self.reference_data.get_internal_load('equipment', load['type'])
674
  load_dict = self.cooling_calculator.calculate_equipment_cooling_load(
675
  power=load['power'],
676
  use_factor=load['usage_factor'],
@@ -689,54 +697,62 @@ class HVACCalculator:
689
  ) if load_type == 'people' else 1.0,
690
  'load': load_dict['total'] / 1000
691
  })
692
-
693
  if st.session_state.get('debug_mode', False):
694
  st.write("Debug: Cooling Results", {
695
  'total_load': results.get('total_load', 'N/A'),
696
  'component_loads': results.get('component_loads', 'N/A'),
697
  'detailed_loads': {k: len(v) if isinstance(v, list) else v for k, v in results.get('detailed_loads', {}).items()}
698
  })
699
-
700
  return True, "Cooling calculation completed.", results
701
-
702
  except ValueError as ve:
703
- logger.error(f"Input error in cooling calculation: {str(ve)}")
704
  return False, f"Input error: {str(ve)}", {}
705
  except KeyError as ke:
706
- logger.error(f"Missing data in cooling calculation: {str(ke)}")
707
  return False, f"Missing data: {str(ke)}", {}
708
  except Exception as e:
709
- logger.error(f"Unexpected error in cooling calculation: {str(e)}")
710
  return False, f"Unexpected error: {str(e)}", {}
711
-
712
  def calculate_heating(self) -> Tuple[bool, str, Dict]:
713
  """
714
  Calculate heating loads using HeatingLoadCalculator.
715
  Returns: (success, message, results)
716
  """
717
  try:
 
718
  valid, message = self.validate_calculation_inputs()
719
  if not valid:
720
  return False, message, {}
721
-
 
722
  building_components = st.session_state.get('components', {})
723
  internal_loads = st.session_state.get('internal_loads', {})
724
  building_info = st.session_state.get('building_info', {})
725
-
 
 
 
 
 
726
  country = building_info.get('country', '').strip().title()
727
  city = building_info.get('city', '').strip().title()
728
  if not country or not city:
729
  return False, "Country and city must be set in Building Information.", {}
730
-
731
  climate_id = self.generate_climate_id(country, city)
732
  location = self.climate_data.get_location_by_id(climate_id, st.session_state)
733
  if not location:
734
  available_locations = list(self.climate_data.locations.keys())[:5]
735
  return False, f"No climate data for {climate_id}. Available locations: {', '.join(available_locations)}...", {}
736
-
 
737
  if not all(k in location for k in ['winter_design_temp', 'monthly_temps', 'monthly_humidity']):
738
  return False, f"Invalid climate data for {climate_id}. Missing required fields.", {}
739
-
 
740
  outdoor_conditions = {
741
  'design_temperature': location['winter_design_temp'],
742
  'design_relative_humidity': location['monthly_humidity'].get('Jan', 80.0),
@@ -747,20 +763,38 @@ class HVACCalculator:
747
  'temperature': building_info.get('indoor_temp', 21.0),
748
  'relative_humidity': building_info.get('indoor_rh', 40.0)
749
  }
750
-
751
  if st.session_state.get('debug_mode', False):
752
  st.write("Debug: Heating Input State", {
753
  'climate_id': climate_id,
754
  'outdoor_conditions': outdoor_conditions,
755
  'indoor_conditions': indoor_conditions,
756
  'components': {k: len(v) for k, v in building_components.items()},
757
- 'internal_loads': {k: len(v) for k, v in internal_loads.items()}
 
 
 
 
 
758
  })
759
-
 
760
  formatted_internal_loads = {
761
- 'people': [],
762
- 'lights': [],
763
- 'equipment': [],
 
 
 
 
 
 
 
 
 
 
 
 
764
  'infiltration': {
765
  'flow_rate': building_info.get('infiltration_rate', 0.05),
766
  'height': building_info.get('building_height', 3.0),
@@ -772,33 +806,8 @@ class HVACCalculator:
772
  'usage_factor': 0.7,
773
  'operating_hours': building_info.get('operating_hours', '8:00-18:00')
774
  }
775
-
776
- for load in internal_loads.get('people', []):
777
- ref_load = self.reference_data.get_internal_load('occupancy', load['activity_level'])
778
- formatted_internal_loads['people'].append({
779
- 'number': load['num_people'],
780
- 'sensible_heat': ref_load.get('sensible_heat', 70) if ref_load else 70,
781
- 'hours_operation': f"{load['hours_in_operation']}:00-{load['hours_in_operation']+10}:00"
782
- })
783
-
784
- for load in internal_loads.get('lighting', []):
785
- ref_load = self.reference_data.get_internal_load('lighting', load['type'])
786
- formatted_internal_loads['lights'].append({
787
- 'power': load['power'],
788
- 'use_factor': load['usage_factor'],
789
- 'heat_to_space': ref_load.get('heat_to_space', 0.9) if ref_load else 0.9,
790
- 'hours_operation': f"{load['hours_in_operation']}h"
791
- })
792
-
793
- for load in internal_loads.get('equipment', []):
794
- ref_load = self.reference_data.get_internal_load('equipment', load['type'])
795
- formatted_internal_loads['equipment'].append({
796
- 'power': load['power'],
797
- 'use_factor': load['usage_factor'],
798
- 'sensible_fraction': ref_load.get('sensible_fraction', 0.9) if ref_load else 0.9,
799
- 'hours_operation': f"{load['hours_in_operation']}h"
800
- })
801
-
802
  design_loads = self.heating_calculator.calculate_design_heating_load(
803
  building_components=building_components,
804
  outdoor_conditions=outdoor_conditions,
@@ -806,12 +815,14 @@ class HVACCalculator:
806
  internal_loads=formatted_internal_loads
807
  )
808
  if not design_loads:
809
- return False, "Heating design loads calculation failed.", {}
810
-
 
811
  summary = self.heating_calculator.calculate_heating_load_summary(design_loads)
812
  if not summary:
813
- return False, "Heating load summary calculation failed.", {}
814
-
 
815
  floor_area = building_info.get('floor_area', 100.0) or 100.0
816
  results = {
817
  'total_load': summary['total'] / 1000, # kW
@@ -846,7 +857,8 @@ class HVACCalculator:
846
  },
847
  'building_info': building_info
848
  }
849
-
 
850
  delta_t = indoor_conditions['temperature'] - outdoor_conditions['design_temperature']
851
  for wall in building_components.get('walls', []):
852
  load = self.heating_calculator.calculate_wall_heating_load(
@@ -862,7 +874,7 @@ class HVACCalculator:
862
  'delta_t': delta_t,
863
  'load': load / 1000
864
  })
865
-
866
  for roof in building_components.get('roofs', []):
867
  load = self.heating_calculator.calculate_roof_heating_load(
868
  roof=roof,
@@ -877,7 +889,7 @@ class HVACCalculator:
877
  'delta_t': delta_t,
878
  'load': load / 1000
879
  })
880
-
881
  for floor in building_components.get('floors', []):
882
  load = self.heating_calculator.calculate_floor_heating_load(
883
  floor=floor,
@@ -891,7 +903,7 @@ class HVACCalculator:
891
  'delta_t': indoor_conditions['temperature'] - outdoor_conditions['ground_temperature'],
892
  'load': load / 1000
893
  })
894
-
895
  for window in building_components.get('windows', []):
896
  load = self.heating_calculator.calculate_window_heating_load(
897
  window=window,
@@ -906,7 +918,7 @@ class HVACCalculator:
906
  'delta_t': delta_t,
907
  'load': load / 1000
908
  })
909
-
910
  for door in building_components.get('doors', []):
911
  load = self.heating_calculator.calculate_door_heating_load(
912
  door=door,
@@ -921,66 +933,71 @@ class HVACCalculator:
921
  'delta_t': delta_t,
922
  'load': load / 1000
923
  })
924
-
925
  if st.session_state.get('debug_mode', False):
926
  st.write("Debug: Heating Results", {
927
  'total_load': results.get('total_load', 'N/A'),
928
  'component_loads': results.get('component_loads', 'N/A'),
929
  'detailed_loads': {k: len(v) if isinstance(v, list) else v for k, v in results.get('detailed_loads', {}).items()}
930
  })
931
-
932
  return True, "Heating calculation completed.", results
933
-
934
  except ValueError as ve:
935
- logger.error(f"Input error in heating calculation: {str(ve)}")
936
  return False, f"Input error: {str(ve)}", {}
937
  except KeyError as ke:
938
- logger.error(f"Missing data in heating calculation: {str(ke)}")
939
  return False, f"Missing data: {str(ke)}", {}
940
  except Exception as e:
941
- logger.error(f"Unexpected error in heating calculation: {str(e)}")
942
  return False, f"Unexpected error: {str(e)}", {}
943
-
944
  def display_calculation_results(self):
945
- """Display calculation results interface."""
946
  st.title("Calculation Results")
947
-
948
  col1, col2 = st.columns(2)
949
  with col1:
950
  calculate_button = st.button("Calculate Loads")
951
  with col2:
952
  st.session_state.debug_mode = st.checkbox("Debug Mode", value=st.session_state.get('debug_mode', False))
953
-
954
  if calculate_button:
 
955
  st.session_state.calculation_results = {'cooling': {}, 'heating': {}}
956
-
957
  with st.spinner("Calculating loads..."):
 
958
  cooling_success, cooling_message, cooling_results = self.calculate_cooling()
959
  if cooling_success:
960
  st.session_state.calculation_results['cooling'] = cooling_results
961
  st.success(cooling_message)
962
  else:
963
  st.error(cooling_message)
964
-
 
965
  heating_success, heating_message, heating_results = self.calculate_heating()
966
  if heating_success:
967
  st.session_state.calculation_results['heating'] = heating_results
968
  st.success(heating_message)
969
  else:
970
  st.error(heating_message)
971
-
 
972
  self.results_display.display_results(st.session_state)
973
-
 
974
  col1, col2 = st.columns(2)
975
  with col1:
976
- st.button("Back to Internal Loads", on_click=lambda: setattr(st.session_state, "page", "Internal Loads"))
 
 
 
977
  with col2:
978
- st.button("Continue to Export Data", on_click=lambda: setattr(st.session_state, "page", "Export Data"))
979
-
 
 
980
 
981
  if __name__ == "__main__":
982
- try:
983
- app = HVACCalculator()
984
- except Exception as e:
985
- logger.error(f"Error initializing HVACCalculator: {str(e)}")
986
- st.error(f"Failed to initialize application: {str(e)}")
 
11
  import pycountry
12
  import os
13
  import sys
 
14
  from typing import Dict, List, Any, Optional, Tuple
 
 
 
 
 
15
 
16
  # Import application modules
17
+ from app.building_info_form import BuildingInfoForm
18
+ from app.component_selection import ComponentSelectionInterface, Orientation, ComponentType, Wall, Roof, Floor, Window, Door
19
+ from app.results_display import ResultsDisplay
20
+ from app.data_validation import DataValidation
21
+ from app.data_persistence import DataPersistence
22
+ from app.data_export import DataExport
 
 
 
 
23
 
24
  # Import data modules
25
+ from data.reference_data import ReferenceData
26
+ from data.climate_data import ClimateData, ClimateLocation
27
+ from data.ashrae_tables import ASHRAETables
28
+ from data.building_components import Wall as WallModel, Roof as RoofModel
 
 
 
 
29
 
30
  # Import utility modules
31
+ from utils.u_value_calculator import UValueCalculator
32
+ from utils.shading_system import ShadingSystem
33
+ from utils.area_calculation_system import AreaCalculationSystem
34
+ from utils.psychrometrics import Psychrometrics
35
+ from utils.heat_transfer import HeatTransferCalculations
36
+ from utils.cooling_load import CoolingLoadCalculator
37
+ from utils.heating_load import HeatingLoadCalculator
38
+ from utils.component_visualization import ComponentVisualization
39
+ from utils.scenario_comparison import ScenarioComparisonVisualization
40
+ from utils.psychrometric_visualization import PsychrometricVisualization
41
+ from utils.time_based_visualization import TimeBasedVisualization
 
 
 
 
 
42
 
43
  class HVACCalculator:
44
  def __init__(self):
45
+ st.set_page_config(
46
+ page_title="HVAC Load Calculator",
47
+ page_icon="🌡️",
48
+ layout="wide",
49
+ initial_sidebar_state="expanded"
50
+ )
51
+
52
+ # Initialize session state
53
+ if 'page' not in st.session_state:
54
+ st.session_state.page = 'Building Information'
55
+
56
+ if 'building_info' not in st.session_state:
57
+ st.session_state.building_info = {"project_name": ""}
58
+
59
+ if 'components' not in st.session_state:
60
+ st.session_state.components = {
61
+ 'walls': [],
62
+ 'roofs': [],
63
+ 'floors': [],
64
+ 'windows': [],
65
+ 'doors': []
66
+ }
67
+
68
+ if 'internal_loads' not in st.session_state:
69
+ st.session_state.internal_loads = {
70
+ 'people': [],
71
+ 'lighting': [],
72
+ 'equipment': []
73
+ }
74
+
75
+ if 'calculation_results' not in st.session_state:
76
+ st.session_state.calculation_results = {
77
+ 'cooling': {},
78
+ 'heating': {}
79
+ }
80
+
81
+ if 'saved_scenarios' not in st.session_state:
82
+ st.session_state.saved_scenarios = {}
83
+
84
+ if 'climate_data' not in st.session_state:
85
+ st.session_state.climate_data = {}
86
+
87
+ if 'debug_mode' not in st.session_state:
88
+ st.session_state.debug_mode = False
89
+
90
+ # Initialize modules
91
+ self.building_info_form = BuildingInfoForm()
92
+ self.component_selection = ComponentSelectionInterface()
93
+ self.results_display = ResultsDisplay()
94
+ self.data_validation = DataValidation()
95
+ self.data_persistence = DataPersistence()
96
+ self.data_export = DataExport()
97
+ self.cooling_calculator = CoolingLoadCalculator()
98
+ self.heating_calculator = HeatingLoadCalculator()
99
+
100
+ # Persist ClimateData in session_state
101
  if 'climate_data_obj' not in st.session_state:
102
  st.session_state.climate_data_obj = ClimateData()
103
  self.climate_data = st.session_state.climate_data_obj
104
+
105
+ # Load default climate data if locations are empty
106
  try:
107
  if not self.climate_data.locations:
108
  self.climate_data = ClimateData.from_json("/home/user/app/climate_data.json")
109
  st.session_state.climate_data_obj = self.climate_data
110
  except FileNotFoundError:
111
  st.warning("Default climate data file not found. Please enter climate data manually.")
112
+
113
+ self.setup_layout()
114
+
 
115
  def setup_layout(self):
 
116
  st.sidebar.title("HVAC Load Calculator")
117
  st.sidebar.markdown("---")
118
+
119
  st.sidebar.subheader("Navigation")
120
  pages = [
121
  "Building Information",
 
125
  "Calculation Results",
126
  "Export Data"
127
  ]
128
+
129
  selected_page = st.sidebar.radio("Go to", pages, index=pages.index(st.session_state.page))
130
+
131
  if selected_page != st.session_state.page:
132
  st.session_state.page = selected_page
133
+
134
  self.display_page(st.session_state.page)
135
+
136
  st.sidebar.markdown("---")
137
  st.sidebar.info(
138
+ "HVAC Load Calculator v1.0.1\n\n"
139
  "Based on ASHRAE steady-state calculation methods\n\n"
140
  "Developed by: Dr Majed Abuseif\n\n"
141
  "School of Architecture and Built Environment\n\n"
142
  "Deakin University\n\n"
143
  "© 2025"
144
  )
145
+
146
  def display_page(self, page: str):
147
+ if page == "Building Information":
148
+ self.building_info_form.display_building_info_form(st.session_state)
149
+ elif page == "Climate Data":
150
+ self.climate_data.display_climate_input(st.session_state)
151
+ elif page == "Building Components":
152
+ self.component_selection.display_component_selection(st.session_state)
153
+ elif page == "Internal Loads":
154
+ self.display_internal_loads()
155
+ elif page == "Calculation Results":
156
+ self.display_calculation_results()
157
+ elif page == "Export Data":
158
+ self.data_export.display()
159
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  def generate_climate_id(self, country: str, city: str) -> str:
161
  """Generate a climate ID from country and city names."""
162
  try:
163
  country = country.strip().title()
164
  city = city.strip().title()
165
  if len(country) < 2 or len(city) < 3:
166
+ raise ValueError("Country and city names must be at least 2 and 3 characters long, respectively.")
167
+ return f"{country[:2].upper()}-{city[:3].upper()}"
 
168
  except Exception as e:
 
169
  raise ValueError(f"Invalid country or city name: {str(e)}")
170
+
171
  def validate_calculation_inputs(self) -> Tuple[bool, str]:
172
  """Validate inputs for cooling and heating calculations."""
173
+ building_info = st.session_state.get('building_info', {})
174
+ components = st.session_state.get('components', {})
175
+ if not building_info.get('floor_area', 0) > 0:
176
+ return False, "Floor area must be positive."
177
+ if not any(components.get(key, []) for key in ['walls', 'roofs', 'windows']):
178
+ return False, "At least one wall, roof, or window must be defined."
179
+ if not st.session_state.get('climate_data'):
180
+ return False, "Climate data is missing."
181
+ for component_type in ['walls', 'roofs', 'windows', 'doors', 'floors']:
182
+ for comp in components.get(component_type, []):
183
+ if comp.area <= 0:
184
+ return False, f"Invalid area for {component_type}: {comp.name}"
185
+ if comp.u_value <= 0:
186
+ return False, f"Invalid U-value for {component_type}: {comp.name}"
187
+ return True, "Inputs valid."
188
+
 
 
 
 
 
 
 
 
189
  def validate_internal_load(self, load_type: str, new_load: Dict) -> Tuple[bool, str]:
190
  """Validate if a new internal load is unique and within limits."""
191
+ loads = st.session_state.internal_loads.get(load_type, [])
192
+ max_loads = 50
193
+
194
+ if len(loads) >= max_loads:
195
+ return False, f"Maximum of {max_loads} {load_type} loads reached."
196
+
197
+ # Check for duplicates based on key attributes
198
+ for existing_load in loads:
199
+ if load_type == 'people':
200
+ if (existing_load['name'] == new_load['name'] and
201
+ existing_load['num_people'] == new_load['num_people'] and
202
+ existing_load['activity_level'] == new_load['activity_level'] and
203
+ existing_load['zone_type'] == new_load['zone_type'] and
204
+ existing_load['hours_in_operation'] == new_load['hours_in_operation']):
205
+ return False, f"Duplicate people load '{new_load['name']}' already exists."
206
+ elif load_type == 'lighting':
207
+ if (existing_load['name'] == new_load['name'] and
208
+ existing_load['power'] == new_load['power'] and
209
+ existing_load['usage_factor'] == new_load['usage_factor'] and
210
+ existing_load['zone_type'] == new_load['zone_type'] and
211
+ existing_load['hours_in_operation'] == new_load['hours_in_operation']):
212
+ return False, f"Duplicate lighting load '{new_load['name']}' already exists."
213
+ elif load_type == 'equipment':
214
+ if (existing_load['name'] == new_load['name'] and
215
+ existing_load['power'] == new_load['power'] and
216
+ existing_load['usage_factor'] == new_load['usage_factor'] and
217
+ existing_load['radiation_fraction'] == new_load['radiation_fraction'] and
218
+ existing_load['zone_type'] == new_load['zone_type'] and
219
+ existing_load['hours_in_operation'] == new_load['hours_in_operation']):
220
+ return False, f"Duplicate equipment load '{new_load['name']}' already exists."
221
+
222
+ return True, "Valid load."
223
+
224
  def display_internal_loads(self):
 
225
  st.title("Internal Loads")
226
+
227
+ # Reset button for all internal loads
228
  if st.button("Reset All Internal Loads"):
229
  st.session_state.internal_loads = {'people': [], 'lighting': [], 'equipment': []}
230
  st.success("All internal loads reset!")
231
  st.rerun()
232
+
233
  tabs = st.tabs(["People", "Lighting", "Equipment"])
234
+
235
  with tabs[0]:
236
+ st.subheader("People")
237
+ with st.form("people_form"):
238
+ num_people = st.number_input("Number of People", min_value=0, value=0, step=1)
239
+ activity_level = st.selectbox(
240
+ "Activity Level",
241
+ ["Seated/Resting", "Light Work", "Moderate Work", "Heavy Work"]
242
+ )
243
+ zone_type = st.selectbox("Zone Type", ["Office", "Classroom", "Retail", "Residential"])
244
+ hours_in_operation = st.number_input(
245
+ "Hours in Operation",
246
+ min_value=0.0,
247
+ max_value=24.0,
248
+ value=8.0,
249
+ step=0.5
250
+ )
251
+ people_name = st.text_input("Name", value="Occupants")
252
+
253
+ if st.form_submit_button("Add People Load"):
254
+ people_load = {
255
+ "id": f"people_{len(st.session_state.internal_loads['people'])}",
256
+ "name": people_name,
257
+ "num_people": num_people,
258
+ "activity_level": activity_level,
259
+ "zone_type": zone_type,
260
+ "hours_in_operation": hours_in_operation
261
+ }
262
+ is_valid, message = self.validate_internal_load('people', people_load)
263
+ if is_valid:
264
+ st.session_state.internal_loads['people'].append(people_load)
265
+ st.success("People load added!")
266
+ st.rerun()
267
+ else:
268
+ st.error(message)
269
+
270
+ if st.session_state.internal_loads['people']:
271
+ people_df = pd.DataFrame(st.session_state.internal_loads['people'])
272
+ st.dataframe(people_df, use_container_width=True)
273
+
274
+ selected_people = st.multiselect(
275
+ "Select People Loads to Delete",
276
+ [load['id'] for load in st.session_state.internal_loads['people']]
277
+ )
278
+ if st.button("Delete Selected People Loads"):
279
+ st.session_state.internal_loads['people'] = [
280
+ load for load in st.session_state.internal_loads['people']
281
+ if load['id'] not in selected_people
282
+ ]
283
+ st.success("Selected people loads deleted!")
284
+ st.rerun()
285
+
286
  with tabs[1]:
287
+ st.subheader("Lighting")
288
+ with st.form("lighting_form"):
289
+ power = st.number_input("Power (W)", min_value=0.0, value=1000.0, step=100.0)
290
+ usage_factor = st.number_input(
291
+ "Usage Factor",
292
+ min_value=0.0,
293
+ max_value=1.0,
294
+ value=0.8,
295
+ step=0.1
296
+ )
297
+ zone_type = st.selectbox("Zone Type", ["Office", "Classroom", "Retail", "Residential"])
298
+ hours_in_operation = st.number_input(
299
+ "Hours in Operation",
300
+ min_value=0.0,
301
+ max_value=24.0,
302
+ value=8.0,
303
+ step=0.5
304
+ )
305
+ lighting_name = st.text_input("Name", value="General Lighting")
306
+
307
+ if st.form_submit_button("Add Lighting Load"):
308
+ lighting_load = {
309
+ "id": f"lighting_{len(st.session_state.internal_loads['lighting'])}",
310
+ "name": lighting_name,
311
+ "power": power,
312
+ "usage_factor": usage_factor,
313
+ "zone_type": zone_type,
314
+ "hours_in_operation": hours_in_operation
315
+ }
316
+ is_valid, message = self.validate_internal_load('lighting', lighting_load)
317
+ if is_valid:
318
+ st.session_state.internal_loads['lighting'].append(lighting_load)
319
+ st.success("Lighting load added!")
320
+ st.rerun()
321
+ else:
322
+ st.error(message)
323
+
324
+ if st.session_state.internal_loads['lighting']:
325
+ lighting_df = pd.DataFrame(st.session_state.internal_loads['lighting'])
326
+ st.dataframe(lighting_df, use_container_width=True)
327
+
328
+ selected_lighting = st.multiselect(
329
+ "Select Lighting Loads to Delete",
330
+ [load['id'] for load in st.session_state.internal_loads['lighting']]
331
+ )
332
+ if st.button("Delete Selected Lighting Loads"):
333
+ st.session_state.internal_loads['lighting'] = [
334
+ load for load in st.session_state.internal_loads['lighting']
335
+ if load['id'] not in selected_lighting
336
+ ]
337
+ st.success("Selected lighting loads deleted!")
338
+ st.rerun()
339
+
340
  with tabs[2]:
341
+ st.subheader("Equipment")
342
+ with st.form("equipment_form"):
343
+ power = st.number_input("Power (W)", min_value=0.0, value=500.0, step=100.0)
344
+ usage_factor = st.number_input(
345
+ "Usage Factor",
346
+ min_value=0.0,
347
+ max_value=1.0,
348
+ value=0.7,
349
+ step=0.1
350
+ )
351
+ radiation_fraction = st.number_input(
352
+ "Radiation Fraction",
353
+ min_value=0.0,
354
+ max_value=1.0,
355
+ value=0.3,
356
+ step=0.1
357
+ )
358
+ zone_type = st.selectbox("Zone Type", ["Office", "Classroom", "Retail", "Residential"])
359
+ hours_in_operation = st.number_input(
360
+ "Hours in Operation",
361
+ min_value=0.0,
362
+ max_value=24.0,
363
+ value=8.0,
364
+ step=0.5
365
+ )
366
+ equipment_name = st.text_input("Name", value="Office Equipment")
367
+
368
+ if st.form_submit_button("Add Equipment Load"):
369
+ equipment_load = {
370
+ "id": f"equipment_{len(st.session_state.internal_loads['equipment'])}",
371
+ "name": equipment_name,
372
+ "power": power,
373
+ "usage_factor": usage_factor,
374
+ "radiation_fraction": radiation_fraction,
375
+ "zone_type": zone_type,
376
+ "hours_in_operation": hours_in_operation
377
+ }
378
+ is_valid, message = self.validate_internal_load('equipment', equipment_load)
379
+ if is_valid:
380
+ st.session_state.internal_loads['equipment'].append(equipment_load)
381
+ st.success("Equipment load added!")
382
+ st.rerun()
383
+ else:
384
+ st.error(message)
385
+
386
+ if st.session_state.internal_loads['equipment']:
387
+ equipment_df = pd.DataFrame(st.session_state.internal_loads['equipment'])
388
+ st.dataframe(equipment_df, use_container_width=True)
389
+
390
+ selected_equipment = st.multiselect(
391
+ "Select Equipment Loads to Delete",
392
+ [load['id'] for load in st.session_state.internal_loads['equipment']]
393
+ )
394
+ if st.button("Delete Selected Equipment Loads"):
395
+ st.session_state.internal_loads['equipment'] = [
396
+ load for load in st.session_state.internal_loads['equipment']
397
+ if load['id'] not in selected_equipment
398
+ ]
399
+ st.success("Selected equipment loads deleted!")
400
+ st.rerun()
401
+
402
  col1, col2 = st.columns(2)
403
  with col1:
404
+ st.button(
405
+ "Back to Building Components",
406
+ on_click=lambda: setattr(st.session_state, "page", "Building Components")
407
+ )
408
  with col2:
409
+ st.button(
410
+ "Continue to Calculation Results",
411
+ on_click=lambda: setattr(st.session_state, "page", "Calculation Results")
412
+ )
413
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  def calculate_cooling(self) -> Tuple[bool, str, Dict]:
415
  """
416
  Calculate cooling loads using CoolingLoadCalculator.
417
  Returns: (success, message, results)
418
  """
419
  try:
420
+ # Validate inputs
421
  valid, message = self.validate_calculation_inputs()
422
  if not valid:
423
  return False, message, {}
424
+
425
+ # Gather inputs
426
  building_components = st.session_state.get('components', {})
427
  internal_loads = st.session_state.get('internal_loads', {})
428
  building_info = st.session_state.get('building_info', {})
429
+
430
+ # Check climate data
431
+ if "climate_data" not in st.session_state or not st.session_state["climate_data"]:
432
+ return False, "Please enter climate data in the 'Climate Data' page.", {}
433
+
434
+ # Extract climate data
435
  country = building_info.get('country', '').strip().title()
436
  city = building_info.get('city', '').strip().title()
437
  if not country or not city:
438
  return False, "Country and city must be set in Building Information.", {}
 
439
  climate_id = self.generate_climate_id(country, city)
440
  location = self.climate_data.get_location_by_id(climate_id, st.session_state)
441
  if not location:
442
  available_locations = list(self.climate_data.locations.keys())[:5]
443
  return False, f"No climate data for {climate_id}. Available locations: {', '.join(available_locations)}...", {}
444
+
445
+ # Validate climate data
446
  if not all(k in location for k in ['summer_design_temp_db', 'summer_design_temp_wb', 'monthly_temps', 'latitude']):
447
  return False, f"Invalid climate data for {climate_id}. Missing required fields.", {}
448
+
449
+ # Format conditions
450
  outdoor_conditions = {
451
  'temperature': location['summer_design_temp_db'],
452
  'relative_humidity': location['monthly_humidity'].get('Jul', 50.0),
 
454
  'month': 'Jul',
455
  'latitude': f"{location['latitude']}N" if location['latitude'] >= 0 else f"{abs(location['latitude'])}S",
456
  'wind_speed': building_info.get('wind_speed', 4.0),
457
+ 'day_of_year': 204 # Approx. July 23
458
  }
459
  indoor_conditions = {
460
  'temperature': building_info.get('indoor_temp', 24.0),
461
  'relative_humidity': building_info.get('indoor_rh', 50.0)
462
  }
463
+
464
  if st.session_state.get('debug_mode', False):
465
  st.write("Debug: Cooling Input State", {
466
  'climate_id': climate_id,
467
  'outdoor_conditions': outdoor_conditions,
468
  'indoor_conditions': indoor_conditions,
469
  'components': {k: len(v) for k, v in building_components.items()},
470
+ 'internal_loads': {
471
+ 'people': len(internal_loads.get('people', [])),
472
+ 'lighting': len(internal_loads.get('lighting', [])),
473
+ 'equipment': len(internal_loads.get('equipment', []))
474
+ },
475
+ 'building_info': building_info
476
  })
477
+
478
+ # Format internal loads
479
  formatted_internal_loads = {
480
+ 'people': {
481
+ 'number': sum(load['num_people'] for load in internal_loads.get('people', [])),
482
+ 'activity_level': internal_loads.get('people', [{}])[0].get('activity_level', 'Seated/Resting'),
483
+ 'operating_hours': f"{internal_loads.get('people', [{}])[0].get('hours_in_operation', 8)}:00-{internal_loads.get('people', [{}])[0].get('hours_in_operation', 8)+10}:00"
484
+ },
485
+ 'lights': {
486
+ 'power': sum(load['power'] for load in internal_loads.get('lighting', [])),
487
+ 'use_factor': internal_loads.get('lighting', [{}])[0].get('usage_factor', 0.8),
488
+ 'special_allowance': 0.1,
489
+ 'hours_operation': f"{internal_loads.get('lighting', [{}])[0].get('hours_in_operation', 8)}h"
490
+ },
491
+ 'equipment': {
492
+ 'power': sum(load['power'] for load in internal_loads.get('equipment', [])),
493
+ 'use_factor': internal_loads.get('equipment', [{}])[0].get('usage_factor', 0.7),
494
+ 'radiation_factor': internal_loads.get('equipment', [{}])[0].get('radiation_fraction', 0.3),
495
+ 'hours_operation': f"{internal_loads.get('equipment', [{}])[0].get('hours_in_operation', 8)}h"
496
+ },
497
  'infiltration': {
498
  'flow_rate': building_info.get('infiltration_rate', 0.05),
499
  'height': building_info.get('building_height', 3.0),
 
504
  },
505
  'operating_hours': building_info.get('operating_hours', '8:00-18:00')
506
  }
507
+
508
+ # Calculate hourly loads
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
509
  hourly_loads = self.cooling_calculator.calculate_hourly_cooling_loads(
510
  building_components=building_components,
511
  outdoor_conditions=outdoor_conditions,
 
514
  building_volume=building_info.get('floor_area', 100.0) * building_info.get('building_height', 3.0)
515
  )
516
  if not hourly_loads:
517
+ return False, "Cooling hourly loads calculation failed. Check input data.", {}
518
+
519
+ # Get design loads
520
  design_loads = self.cooling_calculator.calculate_design_cooling_load(hourly_loads)
521
  if not design_loads:
522
+ return False, "Cooling design loads calculation failed. Check input data.", {}
523
+
524
+ # Get summary
525
  summary = self.cooling_calculator.calculate_cooling_load_summary(design_loads)
526
  if not summary:
527
+ return False, "Cooling load summary calculation failed. Check input data.", {}
528
+
529
+ # Format results for results_display.py
530
  floor_area = building_info.get('floor_area', 100.0) or 100.0
531
  results = {
532
  'total_load': summary['total'] / 1000, # kW
 
565
  },
566
  'building_info': building_info
567
  }
568
+
569
  # Populate detailed loads
570
  for wall in building_components.get('walls', []):
571
  load = self.cooling_calculator.calculate_wall_cooling_load(
 
593
  ),
594
  'load': load / 1000
595
  })
596
+
597
  for roof in building_components.get('roofs', []):
598
  load = self.cooling_calculator.calculate_roof_cooling_load(
599
  roof=roof,
 
619
  ),
620
  'load': load / 1000
621
  })
622
+
623
  for window in building_components.get('windows', []):
624
  load_dict = self.cooling_calculator.calculate_window_cooling_load(
625
  window=window,
 
647
  ),
648
  'load': load_dict['total'] / 1000
649
  })
650
+
651
  for door in building_components.get('doors', []):
652
  load = self.cooling_calculator.calculate_door_cooling_load(
653
  door=door,
 
662
  'cltd': outdoor_conditions['temperature'] - indoor_conditions['temperature'],
663
  'load': load / 1000
664
  })
665
+
666
  for load_type, key in [('people', 'people'), ('lighting', 'lights'), ('equipment', 'equipment')]:
667
  for load in internal_loads.get(key, []):
668
  if load_type == 'people':
 
669
  load_dict = self.cooling_calculator.calculate_people_cooling_load(
670
  num_people=load['num_people'],
671
  activity_level=load['activity_level'],
672
  hour=design_loads['design_hour']
673
  )
674
  elif load_type == 'lighting':
 
675
  load_dict = {'total': self.cooling_calculator.calculate_lights_cooling_load(
676
  power=load['power'],
677
  use_factor=load['usage_factor'],
 
679
  hour=design_loads['design_hour']
680
  )}
681
  else:
 
682
  load_dict = self.cooling_calculator.calculate_equipment_cooling_load(
683
  power=load['power'],
684
  use_factor=load['usage_factor'],
 
697
  ) if load_type == 'people' else 1.0,
698
  'load': load_dict['total'] / 1000
699
  })
700
+
701
  if st.session_state.get('debug_mode', False):
702
  st.write("Debug: Cooling Results", {
703
  'total_load': results.get('total_load', 'N/A'),
704
  'component_loads': results.get('component_loads', 'N/A'),
705
  'detailed_loads': {k: len(v) if isinstance(v, list) else v for k, v in results.get('detailed_loads', {}).items()}
706
  })
707
+
708
  return True, "Cooling calculation completed.", results
709
+
710
  except ValueError as ve:
711
+ st.error(f"Input error in cooling calculation: {str(ve)}")
712
  return False, f"Input error: {str(ve)}", {}
713
  except KeyError as ke:
714
+ st.error(f"Missing data in cooling calculation: {str(ke)}")
715
  return False, f"Missing data: {str(ke)}", {}
716
  except Exception as e:
717
+ st.error(f"Unexpected error in cooling calculation: {str(e)}")
718
  return False, f"Unexpected error: {str(e)}", {}
719
+
720
  def calculate_heating(self) -> Tuple[bool, str, Dict]:
721
  """
722
  Calculate heating loads using HeatingLoadCalculator.
723
  Returns: (success, message, results)
724
  """
725
  try:
726
+ # Validate inputs
727
  valid, message = self.validate_calculation_inputs()
728
  if not valid:
729
  return False, message, {}
730
+
731
+ # Gather inputs
732
  building_components = st.session_state.get('components', {})
733
  internal_loads = st.session_state.get('internal_loads', {})
734
  building_info = st.session_state.get('building_info', {})
735
+
736
+ # Check climate data
737
+ if "climate_data" not in st.session_state or not st.session_state["climate_data"]:
738
+ return False, "Please enter climate data in the 'Climate Data' page.", {}
739
+
740
+ # Extract climate data
741
  country = building_info.get('country', '').strip().title()
742
  city = building_info.get('city', '').strip().title()
743
  if not country or not city:
744
  return False, "Country and city must be set in Building Information.", {}
 
745
  climate_id = self.generate_climate_id(country, city)
746
  location = self.climate_data.get_location_by_id(climate_id, st.session_state)
747
  if not location:
748
  available_locations = list(self.climate_data.locations.keys())[:5]
749
  return False, f"No climate data for {climate_id}. Available locations: {', '.join(available_locations)}...", {}
750
+
751
+ # Validate climate data
752
  if not all(k in location for k in ['winter_design_temp', 'monthly_temps', 'monthly_humidity']):
753
  return False, f"Invalid climate data for {climate_id}. Missing required fields.", {}
754
+
755
+ # Format conditions
756
  outdoor_conditions = {
757
  'design_temperature': location['winter_design_temp'],
758
  'design_relative_humidity': location['monthly_humidity'].get('Jan', 80.0),
 
763
  'temperature': building_info.get('indoor_temp', 21.0),
764
  'relative_humidity': building_info.get('indoor_rh', 40.0)
765
  }
766
+
767
  if st.session_state.get('debug_mode', False):
768
  st.write("Debug: Heating Input State", {
769
  'climate_id': climate_id,
770
  'outdoor_conditions': outdoor_conditions,
771
  'indoor_conditions': indoor_conditions,
772
  'components': {k: len(v) for k, v in building_components.items()},
773
+ 'internal_loads': {
774
+ 'people': len(internal_loads.get('people', [])),
775
+ 'lighting': len(internal_loads.get('lighting', [])),
776
+ 'equipment': len(internal_loads.get('equipment', []))
777
+ },
778
+ 'building_info': building_info
779
  })
780
+
781
+ # Format internal loads
782
  formatted_internal_loads = {
783
+ 'people': {
784
+ 'number': sum(load['num_people'] for load in internal_loads.get('people', [])),
785
+ 'sensible_gain': 70,
786
+ 'operating_hours': f"{internal_loads.get('people', [{}])[0].get('hours_in_operation', 8)}:00-{internal_loads.get('people', [{}])[0].get('hours_in_operation', 8)+10}:00"
787
+ },
788
+ 'lights': {
789
+ 'power': sum(load['power'] for load in internal_loads.get('lighting', [])),
790
+ 'use_factor': internal_loads.get('lighting', [{}])[0].get('usage_factor', 0.8),
791
+ 'hours_operation': f"{internal_loads.get('lighting', [{}])[0].get('hours_in_operation', 8)}h"
792
+ },
793
+ 'equipment': {
794
+ 'power': sum(load['power'] for load in internal_loads.get('equipment', [])),
795
+ 'use_factor': internal_loads.get('equipment', [{}])[0].get('usage_factor', 0.7),
796
+ 'hours_operation': f"{internal_loads.get('equipment', [{}])[0].get('hours_in_operation', 8)}h"
797
+ },
798
  'infiltration': {
799
  'flow_rate': building_info.get('infiltration_rate', 0.05),
800
  'height': building_info.get('building_height', 3.0),
 
806
  'usage_factor': 0.7,
807
  'operating_hours': building_info.get('operating_hours', '8:00-18:00')
808
  }
809
+
810
+ # Calculate design loads
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
811
  design_loads = self.heating_calculator.calculate_design_heating_load(
812
  building_components=building_components,
813
  outdoor_conditions=outdoor_conditions,
 
815
  internal_loads=formatted_internal_loads
816
  )
817
  if not design_loads:
818
+ return False, "Heating design loads calculation failed. Check input data.", {}
819
+
820
+ # Get summary
821
  summary = self.heating_calculator.calculate_heating_load_summary(design_loads)
822
  if not summary:
823
+ return False, "Heating load summary calculation failed. Check input data.", {}
824
+
825
+ # Format results
826
  floor_area = building_info.get('floor_area', 100.0) or 100.0
827
  results = {
828
  'total_load': summary['total'] / 1000, # kW
 
857
  },
858
  'building_info': building_info
859
  }
860
+
861
+ # Populate detailed loads
862
  delta_t = indoor_conditions['temperature'] - outdoor_conditions['design_temperature']
863
  for wall in building_components.get('walls', []):
864
  load = self.heating_calculator.calculate_wall_heating_load(
 
874
  'delta_t': delta_t,
875
  'load': load / 1000
876
  })
877
+
878
  for roof in building_components.get('roofs', []):
879
  load = self.heating_calculator.calculate_roof_heating_load(
880
  roof=roof,
 
889
  'delta_t': delta_t,
890
  'load': load / 1000
891
  })
892
+
893
  for floor in building_components.get('floors', []):
894
  load = self.heating_calculator.calculate_floor_heating_load(
895
  floor=floor,
 
903
  'delta_t': indoor_conditions['temperature'] - outdoor_conditions['ground_temperature'],
904
  'load': load / 1000
905
  })
906
+
907
  for window in building_components.get('windows', []):
908
  load = self.heating_calculator.calculate_window_heating_load(
909
  window=window,
 
918
  'delta_t': delta_t,
919
  'load': load / 1000
920
  })
921
+
922
  for door in building_components.get('doors', []):
923
  load = self.heating_calculator.calculate_door_heating_load(
924
  door=door,
 
933
  'delta_t': delta_t,
934
  'load': load / 1000
935
  })
936
+
937
  if st.session_state.get('debug_mode', False):
938
  st.write("Debug: Heating Results", {
939
  'total_load': results.get('total_load', 'N/A'),
940
  'component_loads': results.get('component_loads', 'N/A'),
941
  'detailed_loads': {k: len(v) if isinstance(v, list) else v for k, v in results.get('detailed_loads', {}).items()}
942
  })
943
+
944
  return True, "Heating calculation completed.", results
945
+
946
  except ValueError as ve:
947
+ st.error(f"Input error in heating calculation: {str(ve)}")
948
  return False, f"Input error: {str(ve)}", {}
949
  except KeyError as ke:
950
+ st.error(f"Missing data in heating calculation: {str(ke)}")
951
  return False, f"Missing data: {str(ke)}", {}
952
  except Exception as e:
953
+ st.error(f"Unexpected error in heating calculation: {str(e)}")
954
  return False, f"Unexpected error: {str(e)}", {}
955
+
956
  def display_calculation_results(self):
 
957
  st.title("Calculation Results")
958
+
959
  col1, col2 = st.columns(2)
960
  with col1:
961
  calculate_button = st.button("Calculate Loads")
962
  with col2:
963
  st.session_state.debug_mode = st.checkbox("Debug Mode", value=st.session_state.get('debug_mode', False))
964
+
965
  if calculate_button:
966
+ # Reset results
967
  st.session_state.calculation_results = {'cooling': {}, 'heating': {}}
968
+
969
  with st.spinner("Calculating loads..."):
970
+ # Calculate cooling load
971
  cooling_success, cooling_message, cooling_results = self.calculate_cooling()
972
  if cooling_success:
973
  st.session_state.calculation_results['cooling'] = cooling_results
974
  st.success(cooling_message)
975
  else:
976
  st.error(cooling_message)
977
+
978
+ # Calculate heating load
979
  heating_success, heating_message, heating_results = self.calculate_heating()
980
  if heating_success:
981
  st.session_state.calculation_results['heating'] = heating_results
982
  st.success(heating_message)
983
  else:
984
  st.error(heating_message)
985
+
986
+ # Display results
987
  self.results_display.display_results(st.session_state)
988
+
989
+ # Navigation
990
  col1, col2 = st.columns(2)
991
  with col1:
992
+ st.button(
993
+ "Back to Internal Loads",
994
+ on_click=lambda: setattr(st.session_state, "page", "Internal Loads")
995
+ )
996
  with col2:
997
+ st.button(
998
+ "Continue to Export Data",
999
+ on_click=lambda: setattr(st.session_state, "page", "Export Data")
1000
+ )
1001
 
1002
  if __name__ == "__main__":
1003
+ app = HVACCalculator()