mabuseif commited on
Commit
4f78072
·
verified ·
1 Parent(s): 845939b

Upload main.py

Browse files
Files changed (1) hide show
  1. app/main.py +226 -90
app/main.py CHANGED
@@ -1,7 +1,7 @@
1
  """
2
  HVAC Calculator Code Documentation
3
- Updated 2025-04-27: Enhanced climate ID generation, input validation, debug mode, and error handling.
4
- Updated 2025-04-28: Added activity-level-based internal gains, ground temperature validation, ASHRAE 62.1 ventilation rates, negative load prevention, and improved usability.
5
  """
6
 
7
  import streamlit as st
@@ -13,10 +13,11 @@ import pycountry
13
  import os
14
  import sys
15
  from typing import Dict, List, Any, Optional, Tuple
 
16
 
17
  # Import application modules
18
  from app.building_info_form import BuildingInfoForm
19
- from app.component_selection import ComponentSelectionInterface, Orientation, ComponentType, Wall, Roof, Floor, Window, Door
20
  from app.results_display import ResultsDisplay
21
  from app.data_validation import DataValidation
22
  from app.data_persistence import DataPersistence
@@ -26,7 +27,7 @@ from app.data_export import DataExport
26
  from data.reference_data import ReferenceData
27
  from data.climate_data import ClimateData, ClimateLocation
28
  from data.ashrae_tables import ASHRAETables
29
- from data.building_components import Wall as WallModel, Roof as RoofModel
30
 
31
  # Import utility modules
32
  from utils.u_value_calculator import UValueCalculator
@@ -40,6 +41,7 @@ from utils.component_visualization import ComponentVisualization
40
  from utils.scenario_comparison import ScenarioComparisonVisualization
41
  from utils.psychrometric_visualization import PsychrometricVisualization
42
  from utils.time_based_visualization import TimeBasedVisualization
 
43
 
44
  # NEW: ASHRAE 62.1 Ventilation Rates (Table 6.1)
45
  VENTILATION_RATES = {
@@ -72,7 +74,8 @@ class HVACCalculator:
72
  'roofs': [],
73
  'floors': [],
74
  'windows': [],
75
- 'doors': []
 
76
  }
77
 
78
  if 'internal_loads' not in st.session_state:
@@ -106,6 +109,7 @@ class HVACCalculator:
106
  self.data_export = DataExport()
107
  self.cooling_calculator = CoolingLoadCalculator()
108
  self.heating_calculator = HeatingLoadCalculator()
 
109
 
110
  # Persist ClimateData in session_state
111
  if 'climate_data_obj' not in st.session_state:
@@ -145,7 +149,7 @@ class HVACCalculator:
145
 
146
  st.sidebar.markdown("---")
147
  st.sidebar.info(
148
- "HVAC Load Calculator v1.0.1\n\n"
149
  "Based on ASHRAE steady-state calculation methods\n\n"
150
  "Developed by: Dr Majed Abuseif\n\n"
151
  "School of Architecture and Built Environment\n\n"
@@ -187,36 +191,53 @@ class HVACCalculator:
187
  # Check building info
188
  if not building_info.get('floor_area', 0) > 0:
189
  return False, "Floor area must be positive."
190
- if not any(components.get(key, []) for key in ['walls', 'roofs', 'windows']):
191
- return False, "At least one wall, roof, or window must be defined."
192
 
193
- # NEW: Validate climate data using climate_data.py
194
  if not climate_data:
195
  return False, "Climate data is missing."
196
  if not self.climate_data.validate_climate_data(climate_data):
197
  return False, "Invalid climate data format or values."
198
 
199
  # Validate components
200
- for component_type in ['walls', 'roofs', 'windows', 'doors', 'floors']:
201
  for comp in components.get(component_type, []):
202
  if comp.area <= 0:
203
  return False, f"Invalid area for {component_type}: {comp.name}"
204
  if comp.u_value <= 0:
205
  return False, f"Invalid U-value for {component_type}: {comp.name}"
206
- # NEW: Validate ground temperature for floors
207
  if component_type == 'floors' and getattr(comp, 'ground_contact', False):
208
  if not -10 <= comp.ground_temperature_c <= 40:
209
  return False, f"Ground temperature for {comp.name} must be between -10°C and 40°C"
210
- # NEW: Validate perimeter
211
  if getattr(comp, 'perimeter', 0) < 0:
212
  return False, f"Perimeter for {comp.name} cannot be negative"
 
 
 
 
 
 
 
 
213
 
214
- # NEW: Validate ventilation rate
215
  if building_info.get('ventilation_rate', 0) < 0:
216
  return False, "Ventilation rate cannot be negative"
217
  if building_info.get('zone_type', '') == 'Custom' and building_info.get('ventilation_rate', 0) == 0:
218
  return False, "Custom ventilation rate must be specified"
219
 
 
 
 
 
 
 
 
 
 
 
220
  return True, "Inputs valid."
221
 
222
  def validate_internal_load(self, load_type: str, new_load: Dict) -> Tuple[bool, str]:
@@ -227,14 +248,14 @@ class HVACCalculator:
227
  if len(loads) >= max_loads:
228
  return False, f"Maximum of {max_loads} {load_type} loads reached."
229
 
230
- # Check for duplicates based on key attributes
231
  for existing_load in loads:
232
  if load_type == 'people':
233
  if (existing_load['name'] == new_load['name'] and
234
  existing_load['num_people'] == new_load['num_people'] and
235
  existing_load['activity_level'] == new_load['activity_level'] and
236
  existing_load['zone_type'] == new_load['zone_type'] and
237
- existing_load['hours_in_operation'] == new_load['hours_in_operation']):
 
238
  return False, f"Duplicate people load '{new_load['name']}' already exists."
239
  elif load_type == 'lighting':
240
  if (existing_load['name'] == new_load['name'] and
@@ -257,13 +278,12 @@ class HVACCalculator:
257
  def display_internal_loads(self):
258
  st.title("Internal Loads")
259
 
260
- # Reset button for all internal loads
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", "Ventilation"]) # NEW: Added Ventilation tab
267
 
268
  with tabs[0]:
269
  st.subheader("People")
@@ -282,16 +302,21 @@ class HVACCalculator:
282
  )
283
  zone_type = st.selectbox(
284
  "Zone Type",
285
- ["Office", "Classroom", "Retail", "Residential"],
286
- help="Select zone type for occupancy characteristics"
 
 
 
 
 
287
  )
288
- hours_in_operation = st.number_input(
289
- "Hours in Operation",
290
  min_value=0.0,
291
- max_value=24.0,
292
- value=8.0,
293
- step=0.5,
294
- help="Daily hours of occupancy"
295
  )
296
  people_name = st.text_input("Name", value="Occupants")
297
 
@@ -302,7 +327,8 @@ class HVACCalculator:
302
  "num_people": num_people,
303
  "activity_level": activity_level,
304
  "zone_type": zone_type,
305
- "hours_in_operation": hours_in_operation
 
306
  }
307
  is_valid, message = self.validate_internal_load('people', people_load)
308
  if is_valid:
@@ -348,16 +374,13 @@ class HVACCalculator:
348
  )
349
  zone_type = st.selectbox(
350
  "Zone Type",
351
- ["Office", "Classroom", "Retail", "Residential"],
352
- help="Select zone type for lighting characteristics"
353
  )
354
- hours_in_operation = st.number_input(
355
- "Hours in Operation",
356
- min_value=0.0,
357
- max_value=24.0,
358
- value=8.0,
359
- step=0.5,
360
- help="Daily hours of lighting operation"
361
  )
362
  lighting_name = st.text_input("Name", value="General Lighting")
363
 
@@ -422,16 +445,13 @@ class HVACCalculator:
422
  )
423
  zone_type = st.selectbox(
424
  "Zone Type",
425
- ["Office", "Classroom", "Retail", "Residential"],
426
- help="Select zone type for equipment characteristics"
427
  )
428
- hours_in_operation = st.number_input(
429
- "Hours in Operation",
430
- min_value=0.0,
431
- max_value=24.0,
432
- value=8.0,
433
- step=0.5,
434
- help="Daily hours of equipment operation"
435
  )
436
  equipment_name = st.text_input("Name", value="Office Equipment")
437
 
@@ -469,7 +489,7 @@ class HVACCalculator:
469
  st.success("Selected equipment loads deleted!")
470
  st.rerun()
471
 
472
- with tabs[3]: # NEW: Ventilation tab
473
  st.subheader("Ventilation Requirements (ASHRAE 62.1)")
474
  with st.form("ventilation_form"):
475
  col1, col2 = st.columns(2)
@@ -500,26 +520,43 @@ class HVACCalculator:
500
  step=0.1,
501
  help="Custom ventilation rate per floor area (ASHRAE 62.1)"
502
  )
 
 
 
 
 
 
 
 
503
  else:
504
  people_rate = VENTILATION_RATES[zone_type]["people_rate"]
505
  area_rate = VENTILATION_RATES[zone_type]["area_rate"]
506
  st.write(f"People Rate: {people_rate} L/s/person (ASHRAE 62.1)")
507
  st.write(f"Area Rate: {area_rate} L/s/m² (ASHRAE 62.1)")
 
 
 
 
 
 
 
 
508
 
509
  if st.form_submit_button("Save Ventilation Settings"):
510
  total_people = sum(load['num_people'] for load in st.session_state.internal_loads.get('people', []))
511
  floor_area = st.session_state.building_info.get('floor_area', 100.0)
512
- ventilation_rate = (
513
  (total_people * people_rate + floor_area * area_rate) / 1000 # Convert L/s to m³/s
514
  )
 
515
  if ventilation_method == 'Demand-Controlled':
516
- ventilation_rate *= 0.75 # Reduce by 25% for DCV
517
  st.session_state.building_info.update({
518
  'zone_type': zone_type,
519
  'ventilation_method': ventilation_method,
520
- 'ventilation_rate': ventilation_rate
521
  })
522
- st.success(f"Ventilation settings saved! Total rate: {ventilation_rate:.3f} m³/s")
523
 
524
  col1, col2 = st.columns(2)
525
  with col1:
@@ -568,19 +605,25 @@ class HVACCalculator:
568
  if not all(k in location for k in ['summer_design_temp_db', 'summer_design_temp_wb', 'monthly_temps', 'latitude']):
569
  return False, f"Invalid climate data for {climate_id}. Missing required fields.", {}
570
 
 
 
 
 
 
 
571
  # Format conditions
572
  outdoor_conditions = {
573
  'temperature': location['summer_design_temp_db'],
574
- 'relative_humidity': location['monthly_humidity'].get('Jul', 50.0),
575
  'ground_temperature': location['monthly_temps'].get('Jul', 20.0),
576
  'month': 'Jul',
577
- 'latitude': location['latitude'], # Pass raw latitude value, validation will happen in cooling_load.py
578
- 'wind_speed': building_info.get('wind_speed', 4.0),
579
- 'day_of_year': 204 # Approx. July 23
580
  }
581
  indoor_conditions = {
582
  'temperature': building_info.get('indoor_temp', 24.0),
583
- 'relative_humidity': building_info.get('indoor_rh', 50.0)
584
  }
585
 
586
  if st.session_state.get('debug_mode', False):
@@ -602,27 +645,31 @@ class HVACCalculator:
602
  'people': {
603
  'number': sum(load['num_people'] for load in internal_loads.get('people', [])),
604
  'activity_level': internal_loads.get('people', [{}])[0].get('activity_level', 'Seated/Resting'),
605
- '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"
 
 
606
  },
607
  'lights': {
608
  'power': sum(load['power'] for load in internal_loads.get('lighting', [])),
609
  'use_factor': internal_loads.get('lighting', [{}])[0].get('usage_factor', 0.8),
610
  'special_allowance': 0.1,
611
- 'hours_operation': f"{internal_loads.get('lighting', [{}])[0].get('hours_in_operation', 8)}h"
 
612
  },
613
  'equipment': {
614
  'power': sum(load['power'] for load in internal_loads.get('equipment', [])),
615
  'use_factor': internal_loads.get('equipment', [{}])[0].get('usage_factor', 0.7),
616
  'radiation_factor': internal_loads.get('equipment', [{}])[0].get('radiation_fraction', 0.3),
617
- 'hours_operation': f"{internal_loads.get('equipment', [{}])[0].get('hours_in_operation', 8)}h"
 
618
  },
619
  'infiltration': {
620
  'flow_rate': building_info.get('infiltration_rate', 0.05),
621
- 'height': building_info.get('building_height', 3.0),
622
  'crack_length': building_info.get('crack_length', 10.0)
623
  },
624
  'ventilation': {
625
- 'flow_rate': building_info.get('ventilation_rate', 0.1)
626
  },
627
  'operating_hours': building_info.get('operating_hours', '8:00-18:00')
628
  }
@@ -650,15 +697,13 @@ class HVACCalculator:
650
 
651
  # Ensure summary has all required keys
652
  if 'total' not in summary:
653
- # Calculate total if missing
654
  if 'total_sensible' in summary and 'total_latent' in summary:
655
  summary['total'] = summary['total_sensible'] + summary['total_latent']
656
  else:
657
- # Fallback to sum of design loads if needed
658
  total_load = sum(value for key, value in design_loads.items() if key != 'design_hour')
659
  summary = {
660
- 'total_sensible': total_load * 0.7, # Approximate sensible ratio
661
- 'total_latent': total_load * 0.3, # Approximate latent ratio
662
  'total': total_load
663
  }
664
 
@@ -674,6 +719,7 @@ class HVACCalculator:
674
  'roof': design_loads['roofs'] / 1000,
675
  'windows': (design_loads['windows_conduction'] + design_loads['windows_solar']) / 1000,
676
  'doors': design_loads['doors'] / 1000,
 
677
  'people': (design_loads['people_sensible'] + design_loads['people_latent']) / 1000,
678
  'lighting': design_loads['lights'] / 1000,
679
  'equipment': (design_loads['equipment_sensible'] + design_loads['equipment_latent']) / 1000,
@@ -685,6 +731,7 @@ class HVACCalculator:
685
  'roofs': [],
686
  'windows': [],
687
  'doors': [],
 
688
  'internal': [],
689
  'infiltration': {
690
  'air_flow': formatted_internal_loads['infiltration']['flow_rate'],
@@ -710,18 +757,20 @@ class HVACCalculator:
710
  indoor_temp=indoor_conditions['temperature'],
711
  month=outdoor_conditions['month'],
712
  hour=design_loads['design_hour'],
713
- latitude=outdoor_conditions['latitude']
 
714
  )
715
  results['detailed_loads']['walls'].append({
716
  'name': wall.name,
717
  'orientation': wall.orientation.value,
718
  'area': wall.area,
719
  'u_value': wall.u_value,
 
720
  'cltd': self.cooling_calculator.ashrae_tables.calculate_corrected_cltd_wall(
721
  wall_group=wall.wall_group,
722
  orientation=wall.orientation.value,
723
  hour=design_loads['design_hour'],
724
- color='Dark',
725
  month=outdoor_conditions['month'],
726
  latitude=outdoor_conditions['latitude'],
727
  indoor_temp=indoor_conditions['temperature'],
@@ -737,17 +786,19 @@ class HVACCalculator:
737
  indoor_temp=indoor_conditions['temperature'],
738
  month=outdoor_conditions['month'],
739
  hour=design_loads['design_hour'],
740
- latitude=outdoor_conditions['latitude']
 
741
  )
742
  results['detailed_loads']['roofs'].append({
743
  'name': roof.name,
744
  'orientation': roof.orientation.value,
745
  'area': roof.area,
746
  'u_value': roof.u_value,
 
747
  'cltd': self.cooling_calculator.ashrae_tables.calculate_corrected_cltd_roof(
748
  roof_group=roof.roof_group,
749
  hour=design_loads['design_hour'],
750
- color='Dark',
751
  month=outdoor_conditions['month'],
752
  latitude=outdoor_conditions['latitude'],
753
  indoor_temp=indoor_conditions['temperature'],
@@ -757,6 +808,12 @@ class HVACCalculator:
757
  })
758
 
759
  for window in building_components.get('windows', []):
 
 
 
 
 
 
760
  load_dict = self.cooling_calculator.calculate_window_cooling_load(
761
  window=window,
762
  outdoor_temp=outdoor_conditions['temperature'],
@@ -764,22 +821,27 @@ class HVACCalculator:
764
  month=outdoor_conditions['month'],
765
  hour=design_loads['design_hour'],
766
  latitude=outdoor_conditions['latitude'],
767
- shading_coefficient=window.shading_coefficient
 
 
 
768
  )
769
- # Ensure load_dict has a 'total' key
770
  if 'total' not in load_dict:
771
  if 'conduction' in load_dict and 'solar' in load_dict:
772
  load_dict['total'] = load_dict['conduction'] + load_dict['solar']
773
  else:
774
  load_dict['total'] = window.u_value * window.area * (outdoor_conditions['temperature'] - indoor_conditions['temperature'])
775
 
776
- # Pass latitude directly to get_scl method which has its own validation
777
  results['detailed_loads']['windows'].append({
778
  'name': window.name,
779
  'orientation': window.orientation.value,
780
  'area': window.area,
781
  'u_value': window.u_value,
782
  'shgc': window.shgc,
 
 
 
 
783
  'shading_device': window.shading_device,
784
  'shading_coefficient': window.shading_coefficient,
785
  'scl': self.cooling_calculator.ashrae_tables.get_scl(
@@ -806,15 +868,59 @@ class HVACCalculator:
806
  'load': load / 1000
807
  })
808
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
809
  for load_type, key in [('people', 'people'), ('lighting', 'lights'), ('equipment', 'equipment')]:
810
  for load in internal_loads.get(key, []):
811
  if load_type == 'people':
812
  load_dict = self.cooling_calculator.calculate_people_cooling_load(
813
  num_people=load['num_people'],
814
  activity_level=load['activity_level'],
815
- hour=design_loads['design_hour']
 
816
  )
817
- # Ensure load_dict has a 'total' key
818
  if 'total' not in load_dict and ('sensible' in load_dict or 'latent' in load_dict):
819
  load_dict['total'] = load_dict.get('sensible', 0) + load_dict.get('latent', 0)
820
  elif load_type == 'lighting':
@@ -832,7 +938,6 @@ class HVACCalculator:
832
  radiation_factor=load['radiation_fraction'],
833
  hour=design_loads['design_hour']
834
  )
835
- # Ensure load_dict has a 'total' key
836
  if 'total' not in load_dict and ('sensible' in load_dict or 'latent' in load_dict):
837
  load_dict['total'] = load_dict.get('sensible', 0) + load_dict.get('latent', 0)
838
  results['detailed_loads']['internal'].append({
@@ -841,8 +946,8 @@ class HVACCalculator:
841
  'quantity': load.get('num_people', load.get('power', 1)),
842
  'heat_gain': load_dict.get('sensible', load_dict.get('total', 0)),
843
  'clf': self.cooling_calculator.ashrae_tables.get_clf_people(
844
- zone_type='A',
845
- hours_occupied='6h', # Using valid '6h' instead of dynamic value that might not exist
846
  hour=design_loads['design_hour']
847
  ) if load_type == 'people' else 1.0,
848
  'load': load_dict.get('total', 0) / 1000
@@ -902,7 +1007,7 @@ class HVACCalculator:
902
  if not all(k in location for k in ['winter_design_temp', 'monthly_temps', 'monthly_humidity']):
903
  return False, f"Invalid climate data for {climate_id}. Missing required fields.", {}
904
 
905
- # NEW: Calculate ground temperature from floors or fallback to climate data
906
  ground_contact_floors = [f for f in building_components.get('floors', []) if getattr(f, 'ground_contact', False)]
907
  ground_temperature = (
908
  sum(f.ground_temperature_c for f in ground_contact_floors) / len(ground_contact_floors)
@@ -912,9 +1017,9 @@ class HVACCalculator:
912
  if not -10 <= ground_temperature <= 40:
913
  return False, f"Invalid ground temperature: {ground_temperature}°C", {}
914
 
915
- # NEW: Skip heating calculation if outdoor temp exceeds indoor temp
916
  indoor_temp = building_info.get('indoor_temp', 21.0)
917
- outdoor_temp = location['winter_design_temp']
918
  if outdoor_temp >= indoor_temp:
919
  results = {
920
  'total_load': 0.0,
@@ -927,6 +1032,7 @@ class HVACCalculator:
927
  'floor': 0.0,
928
  'windows': 0.0,
929
  'doors': 0.0,
 
930
  'infiltration': 0.0,
931
  'ventilation': 0.0
932
  },
@@ -936,6 +1042,7 @@ class HVACCalculator:
936
  'floors': [],
937
  'windows': [],
938
  'doors': [],
 
939
  'infiltration': {'air_flow': 0.0, 'delta_t': 0.0, 'load': 0.0},
940
  'ventilation': {'air_flow': 0.0, 'delta_t': 0.0, 'load': 0.0}
941
  },
@@ -945,14 +1052,14 @@ class HVACCalculator:
945
 
946
  # Format conditions
947
  outdoor_conditions = {
948
- 'design_temperature': location['winter_design_temp'],
949
- 'design_relative_humidity': location['monthly_humidity'].get('Jan', 80.0),
950
  'ground_temperature': ground_temperature,
951
- 'wind_speed': building_info.get('wind_speed', 4.0)
952
  }
953
  indoor_conditions = {
954
  'temperature': indoor_temp,
955
- 'relative_humidity': building_info.get('indoor_rh', 40.0)
956
  }
957
 
958
  if st.session_state.get('debug_mode', False):
@@ -969,7 +1076,7 @@ class HVACCalculator:
969
  'building_info': building_info
970
  })
971
 
972
- # NEW: Activity-level-based sensible gains
973
  ACTIVITY_GAINS = {
974
  'Seated/Resting': 70.0, # W/person
975
  'Light Work': 85.0,
@@ -985,25 +1092,29 @@ class HVACCalculator:
985
  internal_loads.get('people', [{}])[0].get('activity_level', 'Seated/Resting'),
986
  70.0
987
  ),
988
- '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"
 
 
989
  },
990
  'lights': {
991
  'power': sum(load['power'] for load in internal_loads.get('lighting', [])),
992
  'use_factor': internal_loads.get('lighting', [{}])[0].get('usage_factor', 0.8),
993
- 'hours_operation': f"{internal_loads.get('lighting', [{}])[0].get('hours_in_operation', 8)}h"
 
994
  },
995
  'equipment': {
996
  'power': sum(load['power'] for load in internal_loads.get('equipment', [])),
997
  'use_factor': internal_loads.get('equipment', [{}])[0].get('usage_factor', 0.7),
998
- 'hours_operation': f"{internal_loads.get('equipment', [{}])[0].get('hours_in_operation', 8)}h"
 
999
  },
1000
  'infiltration': {
1001
  'flow_rate': building_info.get('infiltration_rate', 0.05),
1002
- 'height': building_info.get('building_height', 3.0),
1003
  'crack_length': building_info.get('crack_length', 10.0)
1004
  },
1005
  'ventilation': {
1006
- 'flow_rate': building_info.get('ventilation_rate', 0.1)
1007
  },
1008
  'usage_factor': 0.7,
1009
  'operating_hours': building_info.get('operating_hours', '8:00-18:00')
@@ -1037,6 +1148,7 @@ class HVACCalculator:
1037
  'floor': design_loads['floors'] / 1000,
1038
  'windows': design_loads['windows'] / 1000,
1039
  'doors': design_loads['doors'] / 1000,
 
1040
  'infiltration': (design_loads['infiltration_sensible'] + design_loads['infiltration_latent']) / 1000,
1041
  'ventilation': (design_loads['ventilation_sensible'] + design_loads['ventilation_latent']) / 1000
1042
  },
@@ -1046,6 +1158,7 @@ class HVACCalculator:
1046
  'floors': [],
1047
  'windows': [],
1048
  'doors': [],
 
1049
  'infiltration': {
1050
  'air_flow': formatted_internal_loads['infiltration']['flow_rate'],
1051
  'delta_t': indoor_conditions['temperature'] - outdoor_conditions['design_temperature'],
@@ -1073,6 +1186,7 @@ class HVACCalculator:
1073
  'orientation': wall.orientation.value,
1074
  'area': wall.area,
1075
  'u_value': wall.u_value,
 
1076
  'delta_t': delta_t,
1077
  'load': load / 1000
1078
  })
@@ -1088,6 +1202,7 @@ class HVACCalculator:
1088
  'orientation': roof.orientation.value,
1089
  'area': roof.area,
1090
  'u_value': roof.u_value,
 
1091
  'delta_t': delta_t,
1092
  'load': load / 1000
1093
  })
@@ -1101,7 +1216,7 @@ class HVACCalculator:
1101
  results['detailed_loads']['floors'].append({
1102
  'name': floor.name,
1103
  'area': floor.area,
1104
- 'u_value': floor.u_value,
1105
  'delta_t': indoor_conditions['temperature'] - outdoor_conditions['ground_temperature'],
1106
  'load': load / 1000
1107
  })
@@ -1110,13 +1225,16 @@ class HVACCalculator:
1110
  load = self.heating_calculator.calculate_window_heating_load(
1111
  window=window,
1112
  outdoor_temp=outdoor_conditions['design_temperature'],
1113
- indoor_temp=indoor_conditions['temperature']
 
1114
  )
1115
  results['detailed_loads']['windows'].append({
1116
  'name': window.name,
1117
  'orientation': window.orientation.value,
1118
  'area': window.area,
1119
  'u_value': window.u_value,
 
 
1120
  'delta_t': delta_t,
1121
  'load': load / 1000
1122
  })
@@ -1136,6 +1254,24 @@ class HVACCalculator:
1136
  'load': load / 1000
1137
  })
1138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1139
  if st.session_state.get('debug_mode', False):
1140
  st.write("Debug: Heating Results", {
1141
  'total_load': results.get('total_load', 'N/A'),
 
1
  """
2
  HVAC Calculator Code Documentation
3
+ Updated 2025-05-02: Integrated skylights, surface color, glazing type, frame type, and drapery adjustments from main_new.py.
4
+ Updated 2025-05-02: Enhanced per Plan.txt to include winter design temperature, humidity, building height, ventilation rate, internal load enhancements, and calculation parameters.
5
  """
6
 
7
  import streamlit as st
 
13
  import os
14
  import sys
15
  from typing import Dict, List, Any, Optional, Tuple
16
+ import uuid
17
 
18
  # Import application modules
19
  from app.building_info_form import BuildingInfoForm
20
+ from app.component_selection import ComponentSelectionInterface, Orientation, ComponentType, Wall, Roof, Floor, Window, Door, Skylight, SurfaceColor, GlazingType, FrameType
21
  from app.results_display import ResultsDisplay
22
  from app.data_validation import DataValidation
23
  from app.data_persistence import DataPersistence
 
27
  from data.reference_data import ReferenceData
28
  from data.climate_data import ClimateData, ClimateLocation
29
  from data.ashrae_tables import ASHRAETables
30
+ from data.building_components import Wall as WallModel, Roof as RoofModel, Skylight as SkylightModel
31
 
32
  # Import utility modules
33
  from utils.u_value_calculator import UValueCalculator
 
41
  from utils.scenario_comparison import ScenarioComparisonVisualization
42
  from utils.psychrometric_visualization import PsychrometricVisualization
43
  from utils.time_based_visualization import TimeBasedVisualization
44
+ from utils.drapery import DraperySystem # NEW: Drapery adjustments
45
 
46
  # NEW: ASHRAE 62.1 Ventilation Rates (Table 6.1)
47
  VENTILATION_RATES = {
 
74
  'roofs': [],
75
  'floors': [],
76
  'windows': [],
77
+ 'doors': [],
78
+ 'skylights': [] # NEW: Added skylights
79
  }
80
 
81
  if 'internal_loads' not in st.session_state:
 
109
  self.data_export = DataExport()
110
  self.cooling_calculator = CoolingLoadCalculator()
111
  self.heating_calculator = HeatingLoadCalculator()
112
+ self.drapery_system = DraperySystem() # NEW: Drapery system
113
 
114
  # Persist ClimateData in session_state
115
  if 'climate_data_obj' not in st.session_state:
 
149
 
150
  st.sidebar.markdown("---")
151
  st.sidebar.info(
152
+ "HVAC Load Calculator v1.0.2\n\n"
153
  "Based on ASHRAE steady-state calculation methods\n\n"
154
  "Developed by: Dr Majed Abuseif\n\n"
155
  "School of Architecture and Built Environment\n\n"
 
191
  # Check building info
192
  if not building_info.get('floor_area', 0) > 0:
193
  return False, "Floor area must be positive."
194
+ if not any(components.get(key, []) for key in ['walls', 'roofs', 'windows', 'skylights']):
195
+ return False, "At least one wall, roof, window, or skylight must be defined."
196
 
197
+ # Validate climate data using climate_data.py
198
  if not climate_data:
199
  return False, "Climate data is missing."
200
  if not self.climate_data.validate_climate_data(climate_data):
201
  return False, "Invalid climate data format or values."
202
 
203
  # Validate components
204
+ for component_type in ['walls', 'roofs', 'windows', 'doors', 'floors', 'skylights']:
205
  for comp in components.get(component_type, []):
206
  if comp.area <= 0:
207
  return False, f"Invalid area for {component_type}: {comp.name}"
208
  if comp.u_value <= 0:
209
  return False, f"Invalid U-value for {component_type}: {comp.name}"
210
+ # Validate ground temperature for floors
211
  if component_type == 'floors' and getattr(comp, 'ground_contact', False):
212
  if not -10 <= comp.ground_temperature_c <= 40:
213
  return False, f"Ground temperature for {comp.name} must be between -10°C and 40°C"
 
214
  if getattr(comp, 'perimeter', 0) < 0:
215
  return False, f"Perimeter for {comp.name} cannot be negative"
216
+ # NEW: Validate enhanced window/skylight properties
217
+ if component_type in ['windows', 'skylights']:
218
+ if getattr(comp, 'shgc', 0) <= 0:
219
+ return False, f"Invalid SHGC for {component_type}: {comp.name}"
220
+ if getattr(comp, 'glazing_type', None) is None:
221
+ return False, f"Glazing type missing for {component_type}: {comp.name}"
222
+ if getattr(comp, 'frame_type', None) is None:
223
+ return False, f"Frame type missing for {component_type}: {comp.name}"
224
 
225
+ # Validate ventilation rate
226
  if building_info.get('ventilation_rate', 0) < 0:
227
  return False, "Ventilation rate cannot be negative"
228
  if building_info.get('zone_type', '') == 'Custom' and building_info.get('ventilation_rate', 0) == 0:
229
  return False, "Custom ventilation rate must be specified"
230
 
231
+ # NEW: Validate new inputs from Plan.txt
232
+ if not -50 <= building_info.get('winter_temp', -10) <= 20:
233
+ return False, "Winter design temperature must be -50 to 20°C"
234
+ if not 0 <= building_info.get('outdoor_rh', 50) <= 100:
235
+ return False, "Outdoor relative humidity must be 0-100%"
236
+ if not 0 <= building_info.get('indoor_rh', 50) <= 100:
237
+ return False, "Indoor relative humidity must be 0-100%"
238
+ if not 0 <= building_info.get('building_height', 3) <= 100:
239
+ return False, "Building height must be 0-100m"
240
+
241
  return True, "Inputs valid."
242
 
243
  def validate_internal_load(self, load_type: str, new_load: Dict) -> Tuple[bool, str]:
 
248
  if len(loads) >= max_loads:
249
  return False, f"Maximum of {max_loads} {load_type} loads reached."
250
 
 
251
  for existing_load in loads:
252
  if load_type == 'people':
253
  if (existing_load['name'] == new_load['name'] and
254
  existing_load['num_people'] == new_load['num_people'] and
255
  existing_load['activity_level'] == new_load['activity_level'] and
256
  existing_load['zone_type'] == new_load['zone_type'] and
257
+ existing_load['hours_in_operation'] == new_load['hours_in_operation'] and
258
+ existing_load['latent_gain'] == new_load['latent_gain']): # NEW: Added latent_gain
259
  return False, f"Duplicate people load '{new_load['name']}' already exists."
260
  elif load_type == 'lighting':
261
  if (existing_load['name'] == new_load['name'] and
 
278
  def display_internal_loads(self):
279
  st.title("Internal Loads")
280
 
 
281
  if st.button("Reset All Internal Loads"):
282
  st.session_state.internal_loads = {'people': [], 'lighting': [], 'equipment': []}
283
  st.success("All internal loads reset!")
284
  st.rerun()
285
 
286
+ tabs = st.tabs(["People", "Lighting", "Equipment", "Ventilation"])
287
 
288
  with tabs[0]:
289
  st.subheader("People")
 
302
  )
303
  zone_type = st.selectbox(
304
  "Zone Type",
305
+ ["A", "B", "C", "D"], # NEW: Updated per Plan.txt
306
+ help="Select zone type for CLF accuracy per ASHRAE"
307
+ )
308
+ hours_in_operation = st.selectbox(
309
+ "Hours Occupied", # NEW: Updated label per Plan.txt
310
+ ["2h", "4h", "6h"], # NEW: Updated options per Plan.txt
311
+ help="Select hours of occupancy for CLF calculations"
312
  )
313
+ latent_gain = st.number_input( # NEW: Added per Plan.txt
314
+ "Latent Gain per Person (Btu/h)",
315
  min_value=0.0,
316
+ max_value=500.0,
317
+ value=200.0,
318
+ step=10.0,
319
+ help="Latent heat gain per person per ASHRAE"
320
  )
321
  people_name = st.text_input("Name", value="Occupants")
322
 
 
327
  "num_people": num_people,
328
  "activity_level": activity_level,
329
  "zone_type": zone_type,
330
+ "hours_in_operation": hours_in_operation,
331
+ "latent_gain": latent_gain # NEW: Added per Plan.txt
332
  }
333
  is_valid, message = self.validate_internal_load('people', people_load)
334
  if is_valid:
 
374
  )
375
  zone_type = st.selectbox(
376
  "Zone Type",
377
+ ["A", "B", "C", "D"], # NEW: Updated per Plan.txt
378
+ help="Select zone type for CLF accuracy per ASHRAE"
379
  )
380
+ hours_in_operation = st.selectbox(
381
+ "Hours On", # NEW: Updated label per Plan.txt
382
+ ["8h", "10h", "12h"], # NEW: Updated options per Plan.txt
383
+ help="Select hours of lighting operation for CLF calculations"
 
 
 
384
  )
385
  lighting_name = st.text_input("Name", value="General Lighting")
386
 
 
445
  )
446
  zone_type = st.selectbox(
447
  "Zone Type",
448
+ ["A", "B", "C", "D"], # NEW: Updated per Plan.txt
449
+ help="Select zone type for CLF accuracy per ASHRAE"
450
  )
451
+ hours_in_operation = st.selectbox(
452
+ "Hours Operated", # NEW: Updated label per Plan.txt
453
+ ["2h", "4h", "6h"], # NEW: Updated options per Plan.txt
454
+ help="Select hours of equipment operation for CLF calculations"
 
 
 
455
  )
456
  equipment_name = st.text_input("Name", value="Office Equipment")
457
 
 
489
  st.success("Selected equipment loads deleted!")
490
  st.rerun()
491
 
492
+ with tabs[3]:
493
  st.subheader("Ventilation Requirements (ASHRAE 62.1)")
494
  with st.form("ventilation_form"):
495
  col1, col2 = st.columns(2)
 
520
  step=0.1,
521
  help="Custom ventilation rate per floor area (ASHRAE 62.1)"
522
  )
523
+ ventilation_rate = st.number_input( # NEW: Added per Plan.txt
524
+ "Ventilation Rate (m³/s)",
525
+ min_value=0.0,
526
+ max_value=10.0,
527
+ value=0.0,
528
+ step=0.1,
529
+ help="Total ventilation rate for custom zone type"
530
+ )
531
  else:
532
  people_rate = VENTILATION_RATES[zone_type]["people_rate"]
533
  area_rate = VENTILATION_RATES[zone_type]["area_rate"]
534
  st.write(f"People Rate: {people_rate} L/s/person (ASHRAE 62.1)")
535
  st.write(f"Area Rate: {area_rate} L/s/m² (ASHRAE 62.1)")
536
+ ventilation_rate = st.number_input( # NEW: Added per Plan.txt
537
+ "Ventilation Rate (m³/s)",
538
+ min_value=0.0,
539
+ max_value=10.0,
540
+ value=0.0,
541
+ step=0.1,
542
+ help="Total ventilation rate (override ASHRAE defaults if needed)"
543
+ )
544
 
545
  if st.form_submit_button("Save Ventilation Settings"):
546
  total_people = sum(load['num_people'] for load in st.session_state.internal_loads.get('people', []))
547
  floor_area = st.session_state.building_info.get('floor_area', 100.0)
548
+ calculated_rate = (
549
  (total_people * people_rate + floor_area * area_rate) / 1000 # Convert L/s to m³/s
550
  )
551
+ final_rate = ventilation_rate if ventilation_rate > 0 else calculated_rate
552
  if ventilation_method == 'Demand-Controlled':
553
+ final_rate *= 0.75 # Reduce by 25% for DCV
554
  st.session_state.building_info.update({
555
  'zone_type': zone_type,
556
  'ventilation_method': ventilation_method,
557
+ 'ventilation_rate': final_rate
558
  })
559
+ st.success(f"Ventilation settings saved! Total rate: {final_rate:.3f} m³/s")
560
 
561
  col1, col2 = st.columns(2)
562
  with col1:
 
605
  if not all(k in location for k in ['summer_design_temp_db', 'summer_design_temp_wb', 'monthly_temps', 'latitude']):
606
  return False, f"Invalid climate data for {climate_id}. Missing required fields.", {}
607
 
608
+ # NEW: Month-to-day mapping per Plan.txt
609
+ month_to_day = {
610
+ "Jan": 15, "Feb": 45, "Mar": 74, "Apr": 105, "May": 135, "Jun": 166,
611
+ "Jul": 196, "Aug": 227, "Sep": 258, "Oct": 288, "Nov": 319, "Dec": 350
612
+ }
613
+
614
  # Format conditions
615
  outdoor_conditions = {
616
  'temperature': location['summer_design_temp_db'],
617
+ 'relative_humidity': building_info.get('outdoor_rh', location['monthly_humidity'].get('Jul', 50.0)), # NEW: Use UI input
618
  'ground_temperature': location['monthly_temps'].get('Jul', 20.0),
619
  'month': 'Jul',
620
+ 'latitude': location['latitude'],
621
+ 'wind_speed': building_info.get('wind_speed', 4.0), # NEW: Use UI input
622
+ 'day_of_year': month_to_day.get('Jul', 182) # NEW: Derive from month
623
  }
624
  indoor_conditions = {
625
  'temperature': building_info.get('indoor_temp', 24.0),
626
+ 'relative_humidity': building_info.get('indoor_rh', 50.0) # NEW: Use UI input
627
  }
628
 
629
  if st.session_state.get('debug_mode', False):
 
645
  'people': {
646
  'number': sum(load['num_people'] for load in internal_loads.get('people', [])),
647
  'activity_level': internal_loads.get('people', [{}])[0].get('activity_level', 'Seated/Resting'),
648
+ 'operating_hours': internal_loads.get('people', [{}])[0].get('hours_in_operation', '8h'), # NEW: Use updated hours
649
+ 'zone_type': internal_loads.get('people', [{}])[0].get('zone_type', 'A'), # NEW: Use updated zone type
650
+ 'latent_gain': internal_loads.get('people', [{}])[0].get('latent_gain', 200.0) # NEW: Added latent gain
651
  },
652
  'lights': {
653
  'power': sum(load['power'] for load in internal_loads.get('lighting', [])),
654
  'use_factor': internal_loads.get('lighting', [{}])[0].get('usage_factor', 0.8),
655
  'special_allowance': 0.1,
656
+ 'hours_operation': internal_loads.get('lighting', [{}])[0].get('hours_in_operation', '8h'), # NEW: Use updated hours
657
+ 'zone_type': internal_loads.get('lighting', [{}])[0].get('zone_type', 'A') # NEW: Use updated zone type
658
  },
659
  'equipment': {
660
  'power': sum(load['power'] for load in internal_loads.get('equipment', [])),
661
  'use_factor': internal_loads.get('equipment', [{}])[0].get('usage_factor', 0.7),
662
  'radiation_factor': internal_loads.get('equipment', [{}])[0].get('radiation_fraction', 0.3),
663
+ 'hours_operation': internal_loads.get('equipment', [{}])[0].get('hours_in_operation', '8h'), # NEW: Use updated hours
664
+ 'zone_type': internal_loads.get('equipment', [{}])[0].get('zone_type', 'A') # NEW: Use updated zone type
665
  },
666
  'infiltration': {
667
  'flow_rate': building_info.get('infiltration_rate', 0.05),
668
+ 'height': building_info.get('building_height', 3.0), # NEW: Use UI input
669
  'crack_length': building_info.get('crack_length', 10.0)
670
  },
671
  'ventilation': {
672
+ 'flow_rate': building_info.get('ventilation_rate', 0.1) # NEW: Use UI input
673
  },
674
  'operating_hours': building_info.get('operating_hours', '8:00-18:00')
675
  }
 
697
 
698
  # Ensure summary has all required keys
699
  if 'total' not in summary:
 
700
  if 'total_sensible' in summary and 'total_latent' in summary:
701
  summary['total'] = summary['total_sensible'] + summary['total_latent']
702
  else:
 
703
  total_load = sum(value for key, value in design_loads.items() if key != 'design_hour')
704
  summary = {
705
+ 'total_sensible': total_load * 0.7,
706
+ 'total_latent': total_load * 0.3,
707
  'total': total_load
708
  }
709
 
 
719
  'roof': design_loads['roofs'] / 1000,
720
  'windows': (design_loads['windows_conduction'] + design_loads['windows_solar']) / 1000,
721
  'doors': design_loads['doors'] / 1000,
722
+ 'skylights': design_loads.get('skylights', 0) / 1000, # NEW: Skylights
723
  'people': (design_loads['people_sensible'] + design_loads['people_latent']) / 1000,
724
  'lighting': design_loads['lights'] / 1000,
725
  'equipment': (design_loads['equipment_sensible'] + design_loads['equipment_latent']) / 1000,
 
731
  'roofs': [],
732
  'windows': [],
733
  'doors': [],
734
+ 'skylights': [], # NEW: Skylights
735
  'internal': [],
736
  'infiltration': {
737
  'air_flow': formatted_internal_loads['infiltration']['flow_rate'],
 
757
  indoor_temp=indoor_conditions['temperature'],
758
  month=outdoor_conditions['month'],
759
  hour=design_loads['design_hour'],
760
+ latitude=outdoor_conditions['latitude'],
761
+ surface_color=wall.surface_color # NEW: Surface color
762
  )
763
  results['detailed_loads']['walls'].append({
764
  'name': wall.name,
765
  'orientation': wall.orientation.value,
766
  'area': wall.area,
767
  'u_value': wall.u_value,
768
+ 'surface_color': wall.surface_color, # NEW: Surface color
769
  'cltd': self.cooling_calculator.ashrae_tables.calculate_corrected_cltd_wall(
770
  wall_group=wall.wall_group,
771
  orientation=wall.orientation.value,
772
  hour=design_loads['design_hour'],
773
+ color=wall.surface_color, # NEW: Use surface color
774
  month=outdoor_conditions['month'],
775
  latitude=outdoor_conditions['latitude'],
776
  indoor_temp=indoor_conditions['temperature'],
 
786
  indoor_temp=indoor_conditions['temperature'],
787
  month=outdoor_conditions['month'],
788
  hour=design_loads['design_hour'],
789
+ latitude=outdoor_conditions['latitude'],
790
+ surface_color=roof.surface_color # NEW: Surface color
791
  )
792
  results['detailed_loads']['roofs'].append({
793
  'name': roof.name,
794
  'orientation': roof.orientation.value,
795
  'area': roof.area,
796
  'u_value': roof.u_value,
797
+ 'surface_color': roof.surface_color, # NEW: Surface color
798
  'cltd': self.cooling_calculator.ashrae_tables.calculate_corrected_cltd_roof(
799
  roof_group=roof.roof_group,
800
  hour=design_loads['design_hour'],
801
+ color=roof.surface_color, # NEW: Use surface color
802
  month=outdoor_conditions['month'],
803
  latitude=outdoor_conditions['latitude'],
804
  indoor_temp=indoor_conditions['temperature'],
 
808
  })
809
 
810
  for window in building_components.get('windows', []):
811
+ # NEW: Apply drapery adjustment
812
+ adjusted_shgc = self.drapery_system.adjust_shgc(
813
+ base_shgc=window.shgc,
814
+ glazing_type=window.glazing_type,
815
+ drapery_type=window.drapery_type if hasattr(window, 'drapery_type') else None
816
+ )
817
  load_dict = self.cooling_calculator.calculate_window_cooling_load(
818
  window=window,
819
  outdoor_temp=outdoor_conditions['temperature'],
 
821
  month=outdoor_conditions['month'],
822
  hour=design_loads['design_hour'],
823
  latitude=outdoor_conditions['latitude'],
824
+ shading_coefficient=window.shading_coefficient,
825
+ adjusted_shgc=adjusted_shgc, # NEW: Adjusted SHGC
826
+ glazing_type=window.glazing_type, # NEW: Glazing type
827
+ frame_type=window.frame_type # NEW: Frame type
828
  )
 
829
  if 'total' not in load_dict:
830
  if 'conduction' in load_dict and 'solar' in load_dict:
831
  load_dict['total'] = load_dict['conduction'] + load_dict['solar']
832
  else:
833
  load_dict['total'] = window.u_value * window.area * (outdoor_conditions['temperature'] - indoor_conditions['temperature'])
834
 
 
835
  results['detailed_loads']['windows'].append({
836
  'name': window.name,
837
  'orientation': window.orientation.value,
838
  'area': window.area,
839
  'u_value': window.u_value,
840
  'shgc': window.shgc,
841
+ 'adjusted_shgc': adjusted_shgc, # NEW: Adjusted SHGC
842
+ 'glazing_type': window.glazing_type, # NEW: Glazing type
843
+ 'frame_type': window.frame_type, # NEW: Frame type
844
+ 'drapery_type': window.drapery_type if hasattr(window, 'drapery_type') else 'None', # NEW: Drapery
845
  'shading_device': window.shading_device,
846
  'shading_coefficient': window.shading_coefficient,
847
  'scl': self.cooling_calculator.ashrae_tables.get_scl(
 
868
  'load': load / 1000
869
  })
870
 
871
+ # NEW: Skylights
872
+ for skylight in building_components.get('skylights', []):
873
+ adjusted_shgc = self.drapery_system.adjust_shgc(
874
+ base_shgc=skylight.shgc,
875
+ glazing_type=skylight.glazing_type,
876
+ drapery_type=skylight.drapery_type if hasattr(skylight, 'drapery_type') else None
877
+ )
878
+ load_dict = self.cooling_calculator.calculate_skylight_cooling_load(
879
+ skylight=skylight,
880
+ outdoor_temp=outdoor_conditions['temperature'],
881
+ indoor_temp=indoor_conditions['temperature'],
882
+ month=outdoor_conditions['month'],
883
+ hour=design_loads['design_hour'],
884
+ latitude=outdoor_conditions['latitude'],
885
+ shading_coefficient=skylight.shading_coefficient,
886
+ adjusted_shgc=adjusted_shgc,
887
+ glazing_type=skylight.glazing_type,
888
+ frame_type=skylight.frame_type
889
+ )
890
+ if 'total' not in load_dict:
891
+ if 'conduction' in load_dict and 'solar' in load_dict:
892
+ load_dict['total'] = load_dict['conduction'] + load_dict['solar']
893
+ else:
894
+ load_dict['total'] = skylight.u_value * skylight.area * (outdoor_conditions['temperature'] - indoor_conditions['temperature'])
895
+
896
+ results['detailed_loads']['skylights'].append({
897
+ 'name': skylight.name,
898
+ 'area': skylight.area,
899
+ 'u_value': skylight.u_value,
900
+ 'shgc': skylight.shgc,
901
+ 'adjusted_shgc': adjusted_shgc,
902
+ 'glazing_type': skylight.glazing_type,
903
+ 'frame_type': skylight.frame_type,
904
+ 'drapery_type': skylight.drapery_type if hasattr(skylight, 'drapery_type') else 'None',
905
+ 'shading_coefficient': skylight.shading_coefficient,
906
+ 'scl': self.cooling_calculator.ashrae_tables.get_scl(
907
+ latitude=outdoor_conditions['latitude'],
908
+ month=outdoor_conditions['month'].title(),
909
+ orientation='Horizontal', # Skylights are horizontal
910
+ hour=design_loads['design_hour']
911
+ ),
912
+ 'load': load_dict['total'] / 1000
913
+ })
914
+
915
  for load_type, key in [('people', 'people'), ('lighting', 'lights'), ('equipment', 'equipment')]:
916
  for load in internal_loads.get(key, []):
917
  if load_type == 'people':
918
  load_dict = self.cooling_calculator.calculate_people_cooling_load(
919
  num_people=load['num_people'],
920
  activity_level=load['activity_level'],
921
+ hour=design_loads['design_hour'],
922
+ latent_gain=load.get('latent_gain', 200.0) # NEW: Pass latent gain
923
  )
 
924
  if 'total' not in load_dict and ('sensible' in load_dict or 'latent' in load_dict):
925
  load_dict['total'] = load_dict.get('sensible', 0) + load_dict.get('latent', 0)
926
  elif load_type == 'lighting':
 
938
  radiation_factor=load['radiation_fraction'],
939
  hour=design_loads['design_hour']
940
  )
 
941
  if 'total' not in load_dict and ('sensible' in load_dict or 'latent' in load_dict):
942
  load_dict['total'] = load_dict.get('sensible', 0) + load_dict.get('latent', 0)
943
  results['detailed_loads']['internal'].append({
 
946
  'quantity': load.get('num_people', load.get('power', 1)),
947
  'heat_gain': load_dict.get('sensible', load_dict.get('total', 0)),
948
  'clf': self.cooling_calculator.ashrae_tables.get_clf_people(
949
+ zone_type=load.get('zone_type', 'A'),
950
+ hours_occupied=load.get('hours_in_operation', '6h'),
951
  hour=design_loads['design_hour']
952
  ) if load_type == 'people' else 1.0,
953
  'load': load_dict.get('total', 0) / 1000
 
1007
  if not all(k in location for k in ['winter_design_temp', 'monthly_temps', 'monthly_humidity']):
1008
  return False, f"Invalid climate data for {climate_id}. Missing required fields.", {}
1009
 
1010
+ # Calculate ground temperature
1011
  ground_contact_floors = [f for f in building_components.get('floors', []) if getattr(f, 'ground_contact', False)]
1012
  ground_temperature = (
1013
  sum(f.ground_temperature_c for f in ground_contact_floors) / len(ground_contact_floors)
 
1017
  if not -10 <= ground_temperature <= 40:
1018
  return False, f"Invalid ground temperature: {ground_temperature}°C", {}
1019
 
1020
+ # Skip heating calculation if outdoor temp exceeds indoor temp
1021
  indoor_temp = building_info.get('indoor_temp', 21.0)
1022
+ outdoor_temp = building_info.get('winter_temp', location['winter_design_temp']) # NEW: Use UI input if available
1023
  if outdoor_temp >= indoor_temp:
1024
  results = {
1025
  'total_load': 0.0,
 
1032
  'floor': 0.0,
1033
  'windows': 0.0,
1034
  'doors': 0.0,
1035
+ 'skylights': 0.0, # NEW: Skylights
1036
  'infiltration': 0.0,
1037
  'ventilation': 0.0
1038
  },
 
1042
  'floors': [],
1043
  'windows': [],
1044
  'doors': [],
1045
+ 'skylights': [], # NEW: Skylights
1046
  'infiltration': {'air_flow': 0.0, 'delta_t': 0.0, 'load': 0.0},
1047
  'ventilation': {'air_flow': 0.0, 'delta_t': 0.0, 'load': 0.0}
1048
  },
 
1052
 
1053
  # Format conditions
1054
  outdoor_conditions = {
1055
+ 'design_temperature': outdoor_temp, # NEW: Use UI input
1056
+ 'design_relative_humidity': building_info.get('outdoor_rh', location['monthly_humidity'].get('Jan', 80.0)), # NEW: Use UI input
1057
  'ground_temperature': ground_temperature,
1058
+ 'wind_speed': building_info.get('wind_speed', 4.0) # NEW: Use UI input
1059
  }
1060
  indoor_conditions = {
1061
  'temperature': indoor_temp,
1062
+ 'relative_humidity': building_info.get('indoor_rh', 40.0) # NEW: Use UI input
1063
  }
1064
 
1065
  if st.session_state.get('debug_mode', False):
 
1076
  'building_info': building_info
1077
  })
1078
 
1079
+ # Activity-level-based sensible gains
1080
  ACTIVITY_GAINS = {
1081
  'Seated/Resting': 70.0, # W/person
1082
  'Light Work': 85.0,
 
1092
  internal_loads.get('people', [{}])[0].get('activity_level', 'Seated/Resting'),
1093
  70.0
1094
  ),
1095
+ 'latent_gain': internal_loads.get('people', [{}])[0].get('latent_gain', 200.0), # NEW: Added latent gain
1096
+ 'operating_hours': internal_loads.get('people', [{}])[0].get('hours_in_operation', '8h'), # NEW: Use updated hours
1097
+ 'zone_type': internal_loads.get('people', [{}])[0].get('zone_type', 'A') # NEW: Use updated zone type
1098
  },
1099
  'lights': {
1100
  'power': sum(load['power'] for load in internal_loads.get('lighting', [])),
1101
  'use_factor': internal_loads.get('lighting', [{}])[0].get('usage_factor', 0.8),
1102
+ 'hours_operation': internal_loads.get('lighting', [{}])[0].get('hours_in_operation', '8h'), # NEW: Use updated hours
1103
+ 'zone_type': internal_loads.get('lighting', [{}])[0].get('zone_type', 'A') # NEW: Use updated zone type
1104
  },
1105
  'equipment': {
1106
  'power': sum(load['power'] for load in internal_loads.get('equipment', [])),
1107
  'use_factor': internal_loads.get('equipment', [{}])[0].get('usage_factor', 0.7),
1108
+ 'hours_operation': internal_loads.get('equipment', [{}])[0].get('hours_in_operation', '8h'), # NEW: Use updated hours
1109
+ 'zone_type': internal_loads.get('equipment', [{}])[0].get('zone_type', 'A') # NEW: Use updated zone type
1110
  },
1111
  'infiltration': {
1112
  'flow_rate': building_info.get('infiltration_rate', 0.05),
1113
+ 'height': building_info.get('building_height', 3.0), # NEW: Use UI input
1114
  'crack_length': building_info.get('crack_length', 10.0)
1115
  },
1116
  'ventilation': {
1117
+ 'flow_rate': building_info.get('ventilation_rate', 0.1) # NEW: Use UI input
1118
  },
1119
  'usage_factor': 0.7,
1120
  'operating_hours': building_info.get('operating_hours', '8:00-18:00')
 
1148
  'floor': design_loads['floors'] / 1000,
1149
  'windows': design_loads['windows'] / 1000,
1150
  'doors': design_loads['doors'] / 1000,
1151
+ 'skylights': design_loads.get('skylights', 0) / 1000, # NEW: Skylights
1152
  'infiltration': (design_loads['infiltration_sensible'] + design_loads['infiltration_latent']) / 1000,
1153
  'ventilation': (design_loads['ventilation_sensible'] + design_loads['ventilation_latent']) / 1000
1154
  },
 
1158
  'floors': [],
1159
  'windows': [],
1160
  'doors': [],
1161
+ 'skylights': [], # NEW: Skylights
1162
  'infiltration': {
1163
  'air_flow': formatted_internal_loads['infiltration']['flow_rate'],
1164
  'delta_t': indoor_conditions['temperature'] - outdoor_conditions['design_temperature'],
 
1186
  'orientation': wall.orientation.value,
1187
  'area': wall.area,
1188
  'u_value': wall.u_value,
1189
+ 'surface_color': wall.surface_color, # NEW: Surface color
1190
  'delta_t': delta_t,
1191
  'load': load / 1000
1192
  })
 
1202
  'orientation': roof.orientation.value,
1203
  'area': roof.area,
1204
  'u_value': roof.u_value,
1205
+ 'surface_color': roof.surface_color, # NEW: Surface color
1206
  'delta_t': delta_t,
1207
  'load': load / 1000
1208
  })
 
1216
  results['detailed_loads']['floors'].append({
1217
  'name': floor.name,
1218
  'area': floor.area,
1219
+ 'u_value': floor.area,
1220
  'delta_t': indoor_conditions['temperature'] - outdoor_conditions['ground_temperature'],
1221
  'load': load / 1000
1222
  })
 
1225
  load = self.heating_calculator.calculate_window_heating_load(
1226
  window=window,
1227
  outdoor_temp=outdoor_conditions['design_temperature'],
1228
+ indoor_temp=indoor_conditions['temperature'],
1229
+ frame_type=window.frame_type # NEW: Frame type
1230
  )
1231
  results['detailed_loads']['windows'].append({
1232
  'name': window.name,
1233
  'orientation': window.orientation.value,
1234
  'area': window.area,
1235
  'u_value': window.u_value,
1236
+ 'glazing_type': window.glazing_type, # NEW: Glazing type
1237
+ 'frame_type': window.frame_type, # NEW: Frame type
1238
  'delta_t': delta_t,
1239
  'load': load / 1000
1240
  })
 
1254
  'load': load / 1000
1255
  })
1256
 
1257
+ # NEW: Skylights
1258
+ for skylight in building_components.get('skylights', []):
1259
+ load = self.heating_calculator.calculate_skylight_heating_load(
1260
+ skylight=skylight,
1261
+ outdoor_temp=outdoor_conditions['design_temperature'],
1262
+ indoor_temp=indoor_conditions['temperature'],
1263
+ frame_type=skylight.frame_type
1264
+ )
1265
+ results['detailed_loads']['skylights'].append({
1266
+ 'name': skylight.name,
1267
+ 'area': skylight.area,
1268
+ 'u_value': skylight.u_value,
1269
+ 'glazing_type': skylight.glazing_type,
1270
+ 'frame_type': skylight.frame_type,
1271
+ 'delta_t': delta_t,
1272
+ 'load': load / 1000
1273
+ })
1274
+
1275
  if st.session_state.get('debug_mode', False):
1276
  st.write("Debug: Heating Results", {
1277
  'total_load': results.get('total_load', 'N/A'),