mabuseif commited on
Commit
d17d027
·
verified ·
1 Parent(s): 6c5b116

Update app/hvac_loads.py

Browse files
Files changed (1) hide show
  1. app/hvac_loads.py +246 -260
app/hvac_loads.py CHANGED
@@ -14,12 +14,13 @@ import pandas as pd
14
  from typing import Dict, List, Optional, NamedTuple, Any, Tuple
15
  from enum import Enum
16
  import streamlit as st
 
 
17
  from datetime import datetime
18
  from collections import defaultdict
19
  import logging
20
  import math
21
  from utils.ctf_calculations import CTFCalculator, ComponentType, CTFCoefficients
22
- from app.material_library import Construction, GlazingMaterial, DoorMaterial, Material, MaterialLibrary
23
 
24
  # Configure logging
25
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@@ -53,7 +54,7 @@ class TFMCalculations:
53
  }
54
 
55
  @staticmethod
56
- def calculate_conduction_load(component: Any, outdoor_temp: float, indoor_temp: float, hour: int, mode: str = "none") -> Tuple[float, float]:
57
  """Calculate conduction load for heating and cooling in kW based on mode."""
58
  if mode == "none":
59
  return 0, 0
@@ -64,14 +65,13 @@ class TFMCalculations:
64
  return 0, 0
65
 
66
  # Get CTF coefficients using CTFCalculator
67
- if not hasattr(component, 'ctf'):
68
- component.ctf = CTFCalculator.calculate_ctf_coefficients(component)
69
-
70
- # Initialize history terms (simplified: assume steady-state history)
71
  load = component.u_value * component.area * delta_t
72
- for i in range(len(component.ctf.Y)):
73
- load += component.area * component.ctf.Y[i] * (outdoor_temp - indoor_temp) * np.exp(-i * 3600 / 3600)
74
- load -= component.area * component.ctf.Z[i] * (outdoor_temp - indoor_temp) * np.exp(-i * 3600 / 3600)
75
  cooling_load = load / 1000 if mode == "cooling" else 0
76
  heating_load = -load / 1000 if mode == "heating" else 0
77
  return cooling_load, heating_load
@@ -103,141 +103,142 @@ class TFMCalculations:
103
  c = TFMCalculations.SHGC_COEFFICIENTS[glazing_type]
104
  f_cos_theta = (c[0] + c[1] * cos_theta + c[2] * cos_theta**2 +
105
  c[3] * cos_theta**3 + c[4] * cos_theta**4 + c[5] * cos_theta**5)
106
- return max(min(f_cos_theta, 1.0), 0.0)
107
 
108
  @staticmethod
109
- def get_surface_parameters(component: Any, building_info: Dict, material_library: MaterialLibrary,
110
- project_materials: Dict, project_constructions: Dict,
111
- project_glazing_materials: Dict, project_door_materials: Dict) -> Tuple[float, float, float, Optional[float], float]:
112
  """
113
- Determine surface parameters for a component using new component structure.
 
114
  """
115
  component_name = getattr(component, 'name', 'unnamed_component')
116
 
117
- # Use component's pre-calculated surface properties
118
- surface_tilt = getattr(component, 'tilt', 90.0)
119
- surface_azimuth = getattr(component, 'surface_azimuth', 0.0)
120
- h_o = 17.0 # Default for walls/windows/doors
121
  emissivity = 0.9 # Default for opaque components
122
- absorptivity = 0.6 # Default for opaque components
123
-
124
  try:
125
- # Adjust defaults based on component type
126
  if component.component_type == ComponentType.ROOF:
 
127
  h_o = 23.0
 
 
 
128
  elif component.component_type == ComponentType.SKYLIGHT:
 
129
  h_o = 23.0
 
 
 
130
  elif component.component_type == ComponentType.FLOOR:
131
  surface_tilt = 180.0
 
132
  surface_azimuth = 0.0
 
 
 
 
133
  h_o = 17.0
134
-
135
- # Fetch material properties
136
- if component.component_type in [ComponentType.WALL, ComponentType.ROOF]:
137
- construction_name = getattr(component, 'construction', None)
138
- if not construction_name:
139
- logger.warning(f"No construction defined for {component_name} ({component.component_type.value}). Using defaults.")
140
  else:
141
- construction_obj = (project_constructions.get(construction_name) or
142
- material_library.library_constructions.get(construction_name))
143
- if not construction_obj:
144
- logger.warning(f"Construction {construction_name} not found for {component_name}. Using defaults.")
145
- elif not construction_obj.layers:
146
- logger.warning(f"No layers in construction {construction_name} for {component_name}. Using defaults.")
 
 
 
147
  else:
148
- first_layer = construction_obj.layers[0]
149
- material = (project_materials.get(first_layer['material']) or
150
- material_library.library_materials.get(first_layer['material']))
151
- if material:
152
- absorptivity = getattr(material, 'absorptivity', 0.6)
153
- emissivity = getattr(material, 'emissivity', 0.9)
154
- logger.debug(f"Using first layer material for {component_name}: "
155
- f"absorptivity={absorptivity}, emissivity={emissivity}")
156
-
157
- elif component.component_type == ComponentType.DOOR:
158
- door_material_name = getattr(component, 'door_material', None)
159
- if not door_material_name:
160
- logger.warning(f"No door material defined for {component_name}. Using defaults.")
161
  else:
162
- door_material_obj = (project_door_materials.get(door_material_name) or
163
- material_library.library_door_materials.get(door_material_name))
164
- if not door_material_obj:
165
- logger.warning(f"Door material {door_material_name} not found for {component_name}. Using defaults.")
166
  else:
167
- absorptivity = getattr(door_material_obj, 'absorptivity', 0.6)
168
- emissivity = getattr(door_material_obj, 'emissivity', 0.9)
169
- logger.debug(f"Using door material for {component_name}: "
170
- f"absorptivity={absorptivity}, emissivity={emissivity}")
171
 
172
  elif component.component_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]:
173
- fenestration_name = getattr(component, 'fenestration', None)
174
- if not fenestration_name:
175
- logger.warning(f"No fenestration defined for {component_name}. Using default SHGC=0.7.")
176
  shgc = 0.7
177
  else:
178
- glazing_material_obj = (project_glazing_materials.get(fenestration_name) or
179
- material_library.library_glazing_materials.get(fenestration_name))
180
- if not glazing_material_obj:
181
- logger.warning(f"Fenestration {fenestration_name} not found for {component_name}. Using default SHGC=0.7.")
182
  shgc = 0.7
183
  else:
184
- shgc = getattr(glazing_material_obj, 'shgc', 0.7)
185
- h_o = getattr(glazing_material_obj, 'h_o', h_o)
186
- logger.debug(f"Using glazing material for {component_name}: shgc={shgc}, h_o={h_o}")
187
- emissivity = None
188
 
189
  except Exception as e:
190
  logger.error(f"Error retrieving surface parameters for {component_name}: {str(e)}")
191
  if component.component_type == ComponentType.ROOF:
192
  surface_tilt = 0.0
193
- surface_azimuth = 0.0
194
  h_o = 23.0
 
195
  elif component.component_type == ComponentType.SKYLIGHT:
196
  surface_tilt = 0.0
197
- surface_azimuth = 0.0
198
  h_o = 23.0
 
199
  elif component.component_type == ComponentType.FLOOR:
200
  surface_tilt = 180.0
201
- surface_azimuth = 0.0
202
  h_o = 17.0
 
203
  else:
204
  surface_tilt = 90.0
205
- surface_azimuth = 0.0
206
  h_o = 17.0
207
- absorptivity = 0.6
208
- emissivity = 0.9 if component.component_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.DOOR] else None
 
209
 
210
- logger.info(f"Surface parameters for {component_name}: tilt={surface_tilt:.1f}, azimuth={surface_azimuth:.1f}, h_o={h_o:.1f}")
211
  return surface_tilt, surface_azimuth, h_o, emissivity, absorptivity
212
 
213
  @staticmethod
214
- def calculate_solar_load(component: Any, hourly_data: Dict, hour: int, building_orientation: float, mode: str = "none") -> float:
215
  """Calculate solar load in kW (cooling only) using ASHRAE-compliant solar calculations."""
216
- if mode != "cooling" or component.component_type == ComponentType.FLOOR:
 
 
217
  return 0
218
 
219
  component_name = getattr(component, 'name', 'unnamed_component')
220
 
221
  try:
222
- # Initialize MaterialLibrary if not present
223
- if "material_library" not in st.session_state:
224
- st.session_state.material_library = MaterialLibrary()
225
- logger.info(f"Initialized MaterialLibrary for {component_name}")
226
-
227
- material_library = st.session_state.material_library
228
- project_materials = st.session_state.project_data["materials"]["project"]
229
- project_constructions = st.session_state.project_data["constructions"]["project"]
230
- project_glazing_materials = st.session_state.project_data["fenestrations"]["project"]
231
- project_door_materials = st.session_state.project_data.get("door_materials", {})
232
-
233
- # Get location parameters from climate_data
234
- climate_data = st.session_state.project_data["climate_data"]
235
  latitude = climate_data.get("latitude", 0.0)
236
  longitude = climate_data.get("longitude", 0.0)
237
- timezone = climate_data.get("timezone", 0.0)
238
  ground_reflectivity = climate_data.get("ground_reflectivity", 0.2)
239
 
240
- # Validate inputs
241
  if not -90 <= latitude <= 90:
242
  logger.warning(f"Invalid latitude {latitude} for {component_name}. Using default 0.0.")
243
  latitude = 0.0
@@ -251,9 +252,8 @@ class TFMCalculations:
251
  logger.warning(f"Invalid ground_reflectivity {ground_reflectivity} for {component_name}. Using default 0.2.")
252
  ground_reflectivity = 0.2
253
 
254
- # Ensure hourly_data has required fields
255
- required_fields = ["month", "day", "hour", "global_horizontal_radiation", "direct_normal_radiation",
256
- "diffuse_horizontal_radiation", "dry_bulb"]
257
  if not all(field in hourly_data for field in required_fields):
258
  logger.warning(f"Missing required fields in hourly_data for hour {hour} for {component_name}: {hourly_data}")
259
  return 0
@@ -277,21 +277,15 @@ class TFMCalculations:
277
  logger.info(f"Processing solar for {month}/{day}/{hour} with GHI={ghi}, DNI={dni}, DHI={dhi}, "
278
  f"dry_bulb={outdoor_temp} for {component_name}")
279
 
280
- # Step 1: Local Solar Time (LST)
281
  year = 2025
282
  n = TFMCalculations.day_of_year(month, day, year)
283
  EOT = TFMCalculations.equation_of_time(n)
284
  lambda_std = 15 * timezone
285
  standard_time = hour - 1 + 0.5
286
- LST = standard_time + (4 * (lambda_std - longitude) + EOT) / 60
287
 
288
- # Step 2: Solar Declination (δ)
289
  delta = 23.45 * math.sin(math.radians(360 / 365 * (284 + n)))
290
-
291
- # Step 3: Hour Angle (HRA)
292
  hra = 15 * (LST - 12)
293
-
294
- # Step 4: Solar Altitude (α) and Azimuth (ψ)
295
  phi = math.radians(latitude)
296
  delta_rad = math.radians(delta)
297
  hra_rad = math.radians(hra)
@@ -311,15 +305,13 @@ class TFMCalculations:
311
  logger.info(f"Solar angles for {month}/{day}/{hour}: declination={delta:.2f}, LST={LST:.2f}, "
312
  f"HRA={hra:.2f}, altitude={alpha:.2f}, azimuth={azimuth:.2f} for {component_name}")
313
 
314
- # Step 5: Get surface parameters
315
- building_info = st.session_state.project_data["building_info"]
316
  surface_tilt, surface_azimuth, h_o, emissivity, absorptivity = \
317
  TFMCalculations.get_surface_parameters(
318
  component, building_info, material_library, project_materials,
319
- project_constructions, project_glazing_materials, project_door_materials
320
  )
321
 
322
- # Step 6: Calculate angle of incidence (θ)
323
  alpha_rad = math.radians(alpha)
324
  surface_tilt_rad = math.radians(surface_tilt)
325
  azimuth_rad = math.radians(azimuth)
@@ -334,41 +326,44 @@ class TFMCalculations:
334
  f"surface_tilt={surface_tilt:.2f}, surface_azimuth={surface_azimuth:.2f}, "
335
  f"cos_theta={cos_theta:.4f}")
336
 
337
- # Step 7: Calculate total incident radiation (I_t)
338
  view_factor = (1 - math.cos(surface_tilt_rad)) / 2
339
  ground_reflected = ground_reflectivity * ghi * view_factor
340
- I_t = dni * cos_theta + dhi + ground_reflected if cos_theta > 0 else dhi + ground_reflected
341
-
342
- # Step 8: Calculate solar heat gain
 
 
 
343
  solar_heat_gain = 0.0
344
-
345
  if component.component_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]:
346
- fenestration_name = getattr(component, 'fenestration', None)
347
  shgc = 0.7
348
- if fenestration_name:
349
- glazing_material_obj = (project_glazing_materials.get(fenestration_name) or
350
- material_library.library_glazing_materials.get(fenestration_name))
351
- if glazing_material_obj:
352
- shgc = getattr(glazing_material_obj, 'shgc', 0.7)
353
- h_o = getattr(glazing_material_obj, 'h_o', h_o)
354
-
355
- glazing_type = TFMCalculations.GLAZING_TYPE_MAPPING.get(fenestration_name, "Single Clear")
 
356
  iac = getattr(component, 'shading_coefficient', 1.0)
357
  shgc_dynamic = shgc * TFMCalculations.calculate_dynamic_shgc(glazing_type, cos_theta)
358
  solar_heat_gain = component.area * shgc_dynamic * I_t * iac / 1000
359
-
360
  logger.info(f"Fenestration solar heat gain for {component_name} at {month}/{day}/{hour}: "
361
  f"{solar_heat_gain:.4f} kW (area={component.area}, shgc_dynamic={shgc_dynamic:.4f}, "
362
  f"I_t={I_t:.2f}, iac={iac})")
363
-
364
- elif component.component_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.DOOR]:
365
- surface_resistance = 1 / h_o
366
  solar_heat_gain = component.area * absorptivity * I_t * surface_resistance / 1000
367
-
368
  logger.info(f"Opaque surface solar heat gain for {component_name} at {month}/{day}/{hour}: "
369
  f"{solar_heat_gain:.4f} kW (area={component.area}, absorptivity={absorptivity:.2f}, "
370
  f"I_t={I_t:.2f}, surface_resistance={surface_resistance:.4f})")
371
-
372
  return solar_heat_gain
373
 
374
  except Exception as e:
@@ -380,51 +375,34 @@ class TFMCalculations:
380
  """Calculate total internal load in kW."""
381
  total_load = 0
382
  for group in internal_loads.get("people", []):
383
- activity_data = group.get("activity_data", {})
384
- sensible = (activity_data.get("sensible_min_w", 0) + activity_data.get("sensible_max_w", 0)) / 2
385
- latent = (activity_data.get("latent_min_w", 0) + activity_data.get("latent_max_w", 0)) / 2
386
- schedule = internal_loads["schedules"].get(group.get("schedule", ""), {})
387
- diversity_factor = schedule.get("weekday", [1.0]*24)[hour % 24] if schedule else 1.0
388
- total_load += group.get("num_people", 0) * (sensible + latent) * diversity_factor
389
-
390
  for light in internal_loads.get("lighting", []):
391
- lpd = light.get("lpd", 0)
392
- schedule = internal_loads["schedules"].get(light.get("schedule", ""), {})
393
- fraction = schedule.get("weekday", [1.0]*24)[hour % 24] if schedule else 1.0
394
- lighting_load = lpd * light.get("area", area) * fraction
395
  total_load += lighting_load
396
-
397
  for equip in internal_loads.get("equipment", []):
398
- sensible_gain = equip.get("sensible_gain", 0)
399
- latent_gain = equip.get("latent_gain", 0)
400
- schedule = internal_loads["schedules"].get(equip.get("schedule", ""), {})
401
- fraction = schedule.get("weekday", [1.0]*24)[hour % 24] if schedule else 1.0
402
- equipment_load = (sensible_gain + latent_gain) * equip.get("area", area) * fraction
403
  total_load += equipment_load
404
-
405
  return total_load / 1000
406
 
407
  @staticmethod
408
- def calculate_ventilation_load(internal_loads: Dict, outdoor_temp: float, indoor_temp: float, area: float, building_info: Dict, mode: str = "none") -> Tuple[float, float]:
409
  """Calculate ventilation load for heating and cooling in kW based on mode."""
410
  if mode == "none":
411
  return 0, 0
412
- ventilation = internal_loads.get("ventilation", [])
413
- if not ventilation:
414
- return 0, 0
415
  total_ventilation_flow = 0
416
- for vent in ventilation:
417
- if vent.get("system_type") == "AirChanges/Hour":
418
- design_flow_rate = vent.get("design_flow_rate", 0.3) # ACH
419
- building_height = building_info.get("building_height", 3.0)
420
- volume = area * building_height
421
- flow = design_flow_rate * volume / 3600 # m³/s
422
- else:
423
- flow = vent.get("design_flow_rate", 0.0) / 1000 # L/s to m³/s
424
- schedule = internal_loads["schedules"].get(vent.get("schedule", ""), {})
425
- fraction = schedule.get("weekday", [1.0]*24)[hour % 24] if schedule else 1.0
426
- total_ventilation_flow += flow * fraction
427
-
428
  air_density = 1.2
429
  specific_heat = 1000
430
  delta_t = outdoor_temp - indoor_temp
@@ -438,33 +416,13 @@ class TFMCalculations:
438
  return cooling_load, heating_load
439
 
440
  @staticmethod
441
- def calculate_infiltration_load(internal_loads: Dict, outdoor_temp: float, indoor_temp: float, area: float, building_info: Dict, mode: str = "none") -> Tuple[float, float]:
442
  """Calculate infiltration load for heating and cooling in kW based on mode."""
443
  if mode == "none":
444
  return 0, 0
445
- infiltration = internal_loads.get("infiltration", [])
446
- if not infiltration:
447
- return 0, 0
448
  total_infiltration_flow = 0
449
- for inf in infiltration:
450
- if inf.get("system_type") == "AirChanges/Hour":
451
- design_flow_rate = inf.get("design_flow_rate", 0.5) # ACH
452
- building_height = building_info.get("building_height", 3.0)
453
- volume = area * building_height
454
- flow = design_flow_rate * volume / 3600 # m³/s
455
- elif inf.get("system_type") == "Crack Flow":
456
- ela = inf.get("effective_air_leakage_area", 0.0001) / 10000 # cm² to m²
457
- wind_speed = 4.0
458
- flow = ela * area * wind_speed / 2
459
- else: # Empirical Equations
460
- c = inf.get("flow_coefficient", 0.0001)
461
- n = inf.get("pressure_exponent", 0.65)
462
- delta_t_abs = abs(outdoor_temp - indoor_temp)
463
- flow = c * (delta_t_abs ** n) * area / 3600
464
- schedule = internal_loads["schedules"].get(inf.get("schedule", ""), {})
465
- fraction = schedule.get("weekday", [1.0]*24)[hour % 24] if schedule else 1.0
466
- total_infiltration_flow += flow * fraction
467
-
468
  air_density = 1.2
469
  specific_heat = 1000
470
  delta_t = outdoor_temp - indoor_temp
@@ -472,6 +430,22 @@ class TFMCalculations:
472
  return 0, 0
473
  if mode == "heating" and delta_t >= 0:
474
  return 0, 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
475
  load = total_infiltration_flow * air_density * specific_heat * delta_t / 1000
476
  cooling_load = load if mode == "cooling" else 0
477
  heating_load = -load if mode == "heating" else 0
@@ -486,15 +460,15 @@ class TFMCalculations:
486
 
487
  @staticmethod
488
  def filter_hourly_data(hourly_data: List[Dict], sim_period: Dict, climate_data: Dict) -> List[Dict]:
489
- """Filter hourly data based on simulation period."""
490
  if sim_period.get("type") == "Full Year":
491
  return hourly_data
492
  filtered_data = []
493
  if sim_period.get("type") == "From-to":
494
- start_month = sim_period.get("start_date", {}).get("month", 1)
495
- start_day = sim_period.get("start_date", {}).get("day", 1)
496
- end_month = sim_period.get("end_date", {}).get("month", 12)
497
- end_day = sim_period.get("end_date", {}).get("day", 31)
498
  for data in hourly_data:
499
  month, day = data["month"], data["day"]
500
  if (month > start_month or (month == start_month and day >= start_day)) and \
@@ -510,21 +484,20 @@ class TFMCalculations:
510
 
511
  @staticmethod
512
  def get_indoor_conditions(indoor_conditions: Dict, hour: int, outdoor_temp: float, building_info: Dict) -> Dict:
513
- """Determine indoor conditions based on user settings."""
 
 
 
 
 
514
  if indoor_conditions.get("type") == "Fixed":
515
- mode = "none" if abs(outdoor_temp - 18) < 0.01 else "cooling" if outdoor_temp > 18 else "heating"
516
  if mode == "cooling":
517
- return {
518
- "temperature": building_info.get("summer_indoor_design_temp", 24.0),
519
- "rh": building_info.get("summer_indoor_design_rh", 50.0)
520
- }
521
  elif mode == "heating":
522
- return {
523
- "temperature": building_info.get("winter_indoor_design_temp", 22.0),
524
- "rh": building_info.get("winter_indoor_design_rh", 50.0)
525
- }
526
  else:
527
- return {"temperature": 24.0, "rh": 50.0}
528
  elif indoor_conditions.get("type") == "Time-varying":
529
  schedule = indoor_conditions.get("schedule", [])
530
  if schedule:
@@ -532,24 +505,24 @@ class TFMCalculations:
532
  for entry in schedule:
533
  if entry["hour"] == hour_idx:
534
  return {"temperature": entry["temperature"], "rh": entry["rh"]}
535
- return {"temperature": 24.0, "rh": 50.0}
536
  else: # Adaptive
537
- return {"temperature": TFMCalculations.get_adaptive_comfort_temp(outdoor_temp), "rh": 50.0}
538
 
539
  @staticmethod
540
  def calculate_tfm_loads(components: Dict, hourly_data: List[Dict], indoor_conditions: Dict, internal_loads: Dict, building_info: Dict, sim_period: Dict, hvac_settings: Dict) -> List[Dict]:
541
- """Calculate TFM loads for heating and cooling."""
542
  filtered_data = TFMCalculations.filter_hourly_data(hourly_data, sim_period, building_info)
543
  temp_loads = []
544
  building_orientation = building_info.get("orientation_angle", 0.0)
545
- operating_periods = hvac_settings.get("operating_hours", [{"start": 8, "end": 18}])
 
546
  area = building_info.get("floor_area", 100.0)
 
 
 
 
547
 
548
- if "material_library" not in st.session_state:
549
- st.session_state.material_library = MaterialLibrary()
550
- logger.info("Initialized MaterialLibrary in session_state")
551
-
552
- # Pre-calculate CTF coefficients
553
  for comp_list in components.values():
554
  for comp in comp_list:
555
  comp.ctf = CTFCalculator.calculate_ctf_coefficients(comp)
@@ -559,18 +532,24 @@ class TFMCalculations:
559
  outdoor_temp = hour_data["dry_bulb"]
560
  indoor_cond = TFMCalculations.get_indoor_conditions(indoor_conditions, hour, outdoor_temp, building_info)
561
  indoor_temp = indoor_cond["temperature"]
562
-
563
  conduction_cooling = conduction_heating = solar = internal = ventilation_cooling = ventilation_heating = infiltration_cooling = infiltration_heating = 0
564
- is_operating = any(period["start"] <= hour % 24 <= period["end"] for period in operating_periods)
565
- mode = "none" if abs(outdoor_temp - 18) < 0.01 else "cooling" if outdoor_temp > 18 else "heating"
566
-
 
 
 
 
 
 
567
  if is_operating and mode == "cooling":
568
  for comp_list in components.values():
569
  for comp in comp_list:
570
  cool_load, _ = TFMCalculations.calculate_conduction_load(comp, outdoor_temp, indoor_temp, hour, mode="cooling")
571
  conduction_cooling += cool_load
572
  solar += TFMCalculations.calculate_solar_load(comp, hour_data, hour, building_orientation, mode="cooling")
573
- internal = TFMCalculations.calculate_internal_load(internal_loads, hour, max([p["end"] - p["start"] for p in operating_periods]), area)
 
574
  ventilation_cooling, _ = TFMCalculations.calculate_ventilation_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, mode="cooling")
575
  infiltration_cooling, _ = TFMCalculations.calculate_infiltration_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, mode="cooling")
576
  elif is_operating and mode == "heating":
@@ -578,12 +557,12 @@ class TFMCalculations:
578
  for comp in comp_list:
579
  _, heat_load = TFMCalculations.calculate_conduction_load(comp, outdoor_temp, indoor_temp, hour, mode="heating")
580
  conduction_heating += heat_load
581
- internal = TFMCalculations.calculate_internal_load(internal_loads, hour, max([p["end"] - p["start"] for p in operating_periods]), area)
582
  _, ventilation_heating = TFMCalculations.calculate_ventilation_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, mode="heating")
583
  _, infiltration_heating = TFMCalculations.calculate_infiltration_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, mode="heating")
584
  else:
585
  internal = 0
586
-
587
  logger.info(f"Hour {hour} total loads - conduction: {conduction_cooling:.3f} kW, solar: {solar:.3f} kW, internal: {internal:.3f} kW")
588
 
589
  total_cooling = conduction_cooling + solar + internal + ventilation_cooling + infiltration_cooling
@@ -592,7 +571,6 @@ class TFMCalculations:
592
  total_heating = 0
593
  elif mode == "heating":
594
  total_cooling = 0
595
-
596
  temp_loads.append({
597
  "hour": hour,
598
  "month": hour_data["month"],
@@ -608,7 +586,7 @@ class TFMCalculations:
608
  "total_cooling": total_cooling,
609
  "total_heating": total_heating
610
  })
611
-
612
  loads_by_day = defaultdict(list)
613
  for load in temp_loads:
614
  day_key = (load["month"], load["day"])
@@ -629,59 +607,67 @@ class TFMCalculations:
629
  return final_loads
630
 
631
  def display_hvac_loads_page():
632
- """Display the HVAC Loads page and manage calculations."""
633
- st.title("HVAC Loads")
634
- st.markdown("Calculate cooling and heating loads based on ASHRAE methodology.")
635
-
636
  project_data = st.session_state.project_data
637
  components = project_data.get("components", {})
638
  climate_data = project_data.get("climate_data", {})
 
639
  internal_loads = project_data.get("internal_loads", {})
640
  building_info = project_data.get("building_info", {})
 
 
641
  sim_period = climate_data.get("typical_extreme_periods", {"type": "Full Year"})
642
- indoor_conditions = {"type": "Fixed"} # Default, can be extended with UI
643
- hvac_settings = project_data.get("hvac_loads", {}).get("settings", {"operating_hours": [{"start": 8, "end": 18}]})
644
-
645
- if not climate_data.get("hourly_data"):
646
- st.warning("Please upload climate data in the Climate Data page.")
647
- return
648
-
649
- if not any(components.values()):
650
- st.warning("Please define building components in the Building Components page.")
651
- return
652
-
 
 
653
  if st.button("Calculate HVAC Loads"):
654
- loads = TFMCalculations.calculate_tfm_loads(
655
- components, climate_data["hourly_data"], indoor_conditions,
656
- internal_loads, building_info, sim_period, hvac_settings
657
- )
658
-
659
- # Update session state
660
- project_data["hvac_loads"]["cooling"]["hourly"] = [load for load in loads if load["total_cooling"] > 0]
661
- project_data["hvac_loads"]["heating"]["hourly"] = [load for load in loads if load["total_heating"] > 0]
662
- project_data["hvac_loads"]["cooling"]["peak"] = max((load["total_cooling"] for load in loads), default=0)
663
- project_data["hvac_loads"]["heating"]["peak"] = max((load["total_heating"] for load in loads), default=0)
664
-
665
- # Generate summary tables and charts
666
- cooling_df = pd.DataFrame(project_data["hvac_loads"]["cooling"]["hourly"])
667
- heating_df = pd.DataFrame(project_data["hvac_loads"]["heating"]["hourly"])
668
-
669
- if not cooling_df.empty:
670
- st.subheader("Cooling Loads")
671
- st.dataframe(cooling_df[["month", "day", "hour", "total_cooling", "conduction_cooling", "solar", "internal", "ventilation_cooling", "infiltration_cooling"]])
672
- project_data["hvac_loads"]["cooling"]["summary_tables"] = {"hourly_summary": cooling_df.to_dict()}
673
-
674
- if not heating_df.empty:
675
- st.subheader("Heating Loads")
676
- st.dataframe(heating_df[["month", "day", "hour", "total_heating", "conduction_heating", "internal", "ventilation_heating", "infiltration_heating"]])
677
- project_data["hvac_loads"]["heating"]["summary_tables"] = {"hourly_summary": heating_df.to_dict()}
678
-
679
- # Trigger rerun if needed
680
- if "hvac_loads_rerun_pending" not in st.session_state:
681
- st.session_state.hvac_loads_rerun_pending = False
682
- if st.session_state.hvac_loads_rerun_pending:
683
- st.session_state.hvac_loads_rerun_pending = False
684
- st.rerun()
 
 
 
685
 
686
  if __name__ == "__main__":
687
  display_hvac_loads_page()
 
14
  from typing import Dict, List, Optional, NamedTuple, Any, Tuple
15
  from enum import Enum
16
  import streamlit as st
17
+ from app.materials_library import MaterialLibrary, Material, GlazingMaterial
18
+ from app.construction import Construction
19
  from datetime import datetime
20
  from collections import defaultdict
21
  import logging
22
  import math
23
  from utils.ctf_calculations import CTFCalculator, ComponentType, CTFCoefficients
 
24
 
25
  # Configure logging
26
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
54
  }
55
 
56
  @staticmethod
57
+ def calculate_conduction_load(component, outdoor_temp: float, indoor_temp: float, hour: int, mode: str = "none") -> tuple[float, float]:
58
  """Calculate conduction load for heating and cooling in kW based on mode."""
59
  if mode == "none":
60
  return 0, 0
 
65
  return 0, 0
66
 
67
  # Get CTF coefficients using CTFCalculator
68
+ ctf = CTFCalculator.calculate_ctf_coefficients(component)
69
+
70
+ # Initialize history terms (simplified: assume steady-state history for demonstration)
 
71
  load = component.u_value * component.area * delta_t
72
+ for i in range(len(ctf.Y)):
73
+ load += component.area * ctf.Y[i] * (outdoor_temp - indoor_temp) * np.exp(-i * 3600 / 3600)
74
+ load -= component.area * ctf.Z[i] * (outdoor_temp - indoor_temp) * np.exp(-i * 3600 / 3600)
75
  cooling_load = load / 1000 if mode == "cooling" else 0
76
  heating_load = -load / 1000 if mode == "heating" else 0
77
  return cooling_load, heating_load
 
103
  c = TFMCalculations.SHGC_COEFFICIENTS[glazing_type]
104
  f_cos_theta = (c[0] + c[1] * cos_theta + c[2] * cos_theta**2 +
105
  c[3] * cos_theta**3 + c[4] * cos_theta**4 + c[5] * cos_theta**5)
106
+ return f_cos_theta
107
 
108
  @staticmethod
109
+ def get_surface_parameters(component: Any, building_info: Dict, material_library: MaterialLibrary,
110
+ project_materials: Dict, project_constructions: Dict,
111
+ project_fenestrations: Dict) -> Tuple[float, float, float, Optional[float], float]:
112
  """
113
+ Determine surface parameters (tilt, azimuth, h_o, emissivity, absorptivity) for a component.
114
+ Uses MaterialLibrary for materials/fenestrations and Construction for walls/roofs/floors.
115
  """
116
  component_name = getattr(component, 'name', 'unnamed_component')
117
 
118
+ # Initialize default values
119
+ surface_tilt = 90.0 # Default vertical for walls, windows
120
+ surface_azimuth = 0.0 # Default north-facing
121
+ h_o = 17.0 # Default exterior convection coefficient
122
  emissivity = 0.9 # Default for opaque components
123
+ absorptivity = 0.6 # Default
124
+
125
  try:
126
+ # Set component-specific defaults based on type
127
  if component.component_type == ComponentType.ROOF:
128
+ surface_tilt = getattr(component, 'tilt', 0.0)
129
  h_o = 23.0
130
+ surface_azimuth = getattr(component, 'rotation', 0.0)
131
+ logger.debug(f"Roof component {component_name}: using rotation={surface_azimuth}, tilt={surface_tilt}")
132
+
133
  elif component.component_type == ComponentType.SKYLIGHT:
134
+ surface_tilt = getattr(component, 'tilt', 0.0)
135
  h_o = 23.0
136
+ surface_azimuth = getattr(component, 'rotation', 0.0)
137
+ logger.debug(f"Skylight component {component_name}: using rotation={surface_azimuth}, tilt={surface_tilt}")
138
+
139
  elif component.component_type == ComponentType.FLOOR:
140
  surface_tilt = 180.0
141
+ h_o = 17.0
142
  surface_azimuth = 0.0
143
+ logger.debug(f"Floor component {component_name}: using default azimuth={surface_azimuth}, tilt={surface_tilt}")
144
+
145
+ else: # WALL, WINDOW
146
+ surface_tilt = 90.0
147
  h_o = 17.0
148
+ elevation = getattr(component, 'elevation', None)
149
+ if not elevation:
150
+ logger.warning(f"Component {component_name} ({component.component_type.value}) is missing 'elevation' field. Using default azimuth=0.")
151
+ surface_azimuth = 0.0
 
 
152
  else:
153
+ elevation_angles = {
154
+ "A": building_info.get("orientation_angle", 0.0),
155
+ "B": (building_info.get("orientation_angle", 0.0) + 90.0) % 360,
156
+ "C": (building_info.get("orientation_angle", 0.0) + 180.0) % 360,
157
+ "D": (building_info.get("orientation_angle", 0.0) + 270.0) % 360
158
+ }
159
+ if elevation not in elevation_angles:
160
+ logger.warning(f"Invalid elevation '{elevation}' for component {component_name}. Using default azimuth=0.")
161
+ surface_azimuth = 0.0
162
  else:
163
+ surface_azimuth = (elevation_angles[elevation] + getattr(component, 'rotation', 0.0)) % 360
164
+ logger.debug(f"Component {component_name}: elevation={elevation}, total_azimuth={surface_azimuth}, tilt={surface_tilt}")
165
+
166
+ # Fetch material properties
167
+ if component.component_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.FLOOR]:
168
+ construction = getattr(component, 'construction', None)
169
+ if not construction:
170
+ logger.warning(f"No construction defined for {component_name} ({component.component_type.value}). Using defaults: absorptivity=0.6, emissivity=0.9.")
 
 
 
 
 
171
  else:
172
+ construction_obj = project_constructions.get(construction) or material_library.library_constructions.get(construction)
173
+ if not construction_obj or not construction_obj.layers:
174
+ logger.warning(f"Construction not found or empty for {component_name}. Using defaults.")
 
175
  else:
176
+ absorptivity = getattr(construction_obj, 'absorptivity', 0.6)
177
+ emissivity = getattr(construction_obj, 'emissivity', 0.9)
178
+ logger.debug(f"Using construction for {component_name}: absorptivity={absorptivity}, emissivity={emissivity}")
 
179
 
180
  elif component.component_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]:
181
+ fenestration = getattr(component, 'fenestration', None)
182
+ if not fenestration:
183
+ logger.warning(f"No fenestration defined for {component_name} ({component.component_type.value}). Using default SHGC=0.7, h_o={h_o}.")
184
  shgc = 0.7
185
  else:
186
+ fenestration_obj = project_fenestrations.get(fenestration) or material_library.library_fenestrations.get(fenestration)
187
+ if not fenestration_obj:
188
+ logger.warning(f"Fenestration not found for {component_name}. Using default SHGC=0.7.")
 
189
  shgc = 0.7
190
  else:
191
+ shgc = getattr(fenestration_obj, 'shgc', 0.7)
192
+ h_o = getattr(fenestration_obj, 'h_o', h_o)
193
+ logger.debug(f"Using fenestration for {component_name}: shgc={shgc}, h_o={h_o}")
194
+ emissivity = None # Not used for fenestration
195
 
196
  except Exception as e:
197
  logger.error(f"Error retrieving surface parameters for {component_name}: {str(e)}")
198
  if component.component_type == ComponentType.ROOF:
199
  surface_tilt = 0.0
 
200
  h_o = 23.0
201
+ surface_azimuth = 0.0
202
  elif component.component_type == ComponentType.SKYLIGHT:
203
  surface_tilt = 0.0
 
204
  h_o = 23.0
205
+ surface_azimuth = 0.0
206
  elif component.component_type == ComponentType.FLOOR:
207
  surface_tilt = 180.0
 
208
  h_o = 17.0
209
+ surface_azimuth = 0.0
210
  else:
211
  surface_tilt = 90.0
 
212
  h_o = 17.0
213
+ surface_azimuth = 0.0
214
+ absorptivity = 0.6 if component.component_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.FLOOR] else 0.0
215
+ emissivity = 0.9 if component.component_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.FLOOR] else None
216
 
217
+ logger.info(f"Final surface parameters for {component_name}: tilt={surface_tilt:.1f}, azimuth={surface_azimuth:.1f}, h_o={h_o:.1f}")
218
  return surface_tilt, surface_azimuth, h_o, emissivity, absorptivity
219
 
220
  @staticmethod
221
+ def calculate_solar_load(component, hourly_data: Dict, hour: int, building_orientation: float, mode: str = "none") -> float:
222
  """Calculate solar load in kW (cooling only) using ASHRAE-compliant solar calculations."""
223
+ if mode != "cooling":
224
+ return 0
225
+ if component.component_type == ComponentType.FLOOR:
226
  return 0
227
 
228
  component_name = getattr(component, 'name', 'unnamed_component')
229
 
230
  try:
231
+ material_library = st.session_state.project_data.get("material_library", MaterialLibrary())
232
+ project_materials = st.session_state.project_data.get("materials", {}).get("project", {})
233
+ project_constructions = st.session_state.project_data.get("constructions", {}).get("project", {})
234
+ project_fenestrations = st.session_state.project_data.get("fenestrations", {}).get("project", {})
235
+
236
+ climate_data = st.session_state.project_data.get("climate_data", {})
 
 
 
 
 
 
 
237
  latitude = climate_data.get("latitude", 0.0)
238
  longitude = climate_data.get("longitude", 0.0)
239
+ timezone = climate_data.get("time_zone", 0.0)
240
  ground_reflectivity = climate_data.get("ground_reflectivity", 0.2)
241
 
 
242
  if not -90 <= latitude <= 90:
243
  logger.warning(f"Invalid latitude {latitude} for {component_name}. Using default 0.0.")
244
  latitude = 0.0
 
252
  logger.warning(f"Invalid ground_reflectivity {ground_reflectivity} for {component_name}. Using default 0.2.")
253
  ground_reflectivity = 0.2
254
 
255
+ required_fields = ["month", "day", "hour", "global_horizontal_radiation", "direct_normal_radiation",
256
+ "diffuse_horizontal_radiation", "dry_bulb"]
 
257
  if not all(field in hourly_data for field in required_fields):
258
  logger.warning(f"Missing required fields in hourly_data for hour {hour} for {component_name}: {hourly_data}")
259
  return 0
 
277
  logger.info(f"Processing solar for {month}/{day}/{hour} with GHI={ghi}, DNI={dni}, DHI={dhi}, "
278
  f"dry_bulb={outdoor_temp} for {component_name}")
279
 
 
280
  year = 2025
281
  n = TFMCalculations.day_of_year(month, day, year)
282
  EOT = TFMCalculations.equation_of_time(n)
283
  lambda_std = 15 * timezone
284
  standard_time = hour - 1 + 0.5
285
+ LST = standard_time + (4 * (lambda_std - longitude) + EOT) / 60
286
 
 
287
  delta = 23.45 * math.sin(math.radians(360 / 365 * (284 + n)))
 
 
288
  hra = 15 * (LST - 12)
 
 
289
  phi = math.radians(latitude)
290
  delta_rad = math.radians(delta)
291
  hra_rad = math.radians(hra)
 
305
  logger.info(f"Solar angles for {month}/{day}/{hour}: declination={delta:.2f}, LST={LST:.2f}, "
306
  f"HRA={hra:.2f}, altitude={alpha:.2f}, azimuth={azimuth:.2f} for {component_name}")
307
 
308
+ building_info = {"orientation_angle": building_orientation}
 
309
  surface_tilt, surface_azimuth, h_o, emissivity, absorptivity = \
310
  TFMCalculations.get_surface_parameters(
311
  component, building_info, material_library, project_materials,
312
+ project_constructions, project_fenestrations
313
  )
314
 
 
315
  alpha_rad = math.radians(alpha)
316
  surface_tilt_rad = math.radians(surface_tilt)
317
  azimuth_rad = math.radians(azimuth)
 
326
  f"surface_tilt={surface_tilt:.2f}, surface_azimuth={surface_azimuth:.2f}, "
327
  f"cos_theta={cos_theta:.4f}")
328
 
 
329
  view_factor = (1 - math.cos(surface_tilt_rad)) / 2
330
  ground_reflected = ground_reflectivity * ghi * view_factor
331
+
332
+ if cos_theta > 0:
333
+ I_t = dni * cos_theta + dhi + ground_reflected
334
+ else:
335
+ I_t = dhi + ground_reflected
336
+
337
  solar_heat_gain = 0.0
338
+
339
  if component.component_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]:
340
+ fenestration = getattr(component, 'fenestration', None)
341
  shgc = 0.7
342
+ if fenestration:
343
+ fenestration_obj = project_fenestrations.get(fenestration) or material_library.library_fenestrations.get(fenestration)
344
+ if fenestration_obj:
345
+ shgc = getattr(fenestration_obj, 'shgc', 0.7)
346
+ h_o = getattr(fenestration_obj, 'h_o', h_o)
347
+ else:
348
+ logger.warning(f"Fenestration not found for {component_name}. Using default SHGC=0.7.")
349
+
350
+ glazing_type = TFMCalculations.GLAZING_TYPE_MAPPING.get(fenestration, "Single Clear")
351
  iac = getattr(component, 'shading_coefficient', 1.0)
352
  shgc_dynamic = shgc * TFMCalculations.calculate_dynamic_shgc(glazing_type, cos_theta)
353
  solar_heat_gain = component.area * shgc_dynamic * I_t * iac / 1000
354
+
355
  logger.info(f"Fenestration solar heat gain for {component_name} at {month}/{day}/{hour}: "
356
  f"{solar_heat_gain:.4f} kW (area={component.area}, shgc_dynamic={shgc_dynamic:.4f}, "
357
  f"I_t={I_t:.2f}, iac={iac})")
358
+
359
+ elif component.component_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.FLOOR]:
360
+ surface_resistance = 1/h_o
361
  solar_heat_gain = component.area * absorptivity * I_t * surface_resistance / 1000
362
+
363
  logger.info(f"Opaque surface solar heat gain for {component_name} at {month}/{day}/{hour}: "
364
  f"{solar_heat_gain:.4f} kW (area={component.area}, absorptivity={absorptivity:.2f}, "
365
  f"I_t={I_t:.2f}, surface_resistance={surface_resistance:.4f})")
366
+
367
  return solar_heat_gain
368
 
369
  except Exception as e:
 
375
  """Calculate total internal load in kW."""
376
  total_load = 0
377
  for group in internal_loads.get("people", []):
378
+ activity_data = group["activity_data"]
379
+ sensible = (activity_data["sensible_min_w"] + activity_data["sensible_max_w"]) / 2
380
+ latent = (activity_data["latent_min_w"] + activity_data["latent_max_w"]) / 2
381
+ load_per_person = sensible + latent
382
+ total_load += group["num_people"] * load_per_person * group.get("diversity_factor", 1.0)
 
 
383
  for light in internal_loads.get("lighting", []):
384
+ lpd = light["lpd"]
385
+ lighting_operating_hours = light.get("operating_hours", operation_hours)
386
+ fraction = min(lighting_operating_hours, operation_hours) / operation_hours if operation_hours > 0 else 0
387
+ lighting_load = lpd * light["area"] * fraction
388
  total_load += lighting_load
 
389
  for equip in internal_loads.get("equipment", []):
390
+ equipment_load = equip["equipment_load"] * equip["area"]
 
 
 
 
391
  total_load += equipment_load
 
392
  return total_load / 1000
393
 
394
  @staticmethod
395
+ def calculate_ventilation_load(internal_loads: Dict, outdoor_temp: float, indoor_temp: float, area: float, building_info: Dict, mode: str = "none") -> tuple[float, float]:
396
  """Calculate ventilation load for heating and cooling in kW based on mode."""
397
  if mode == "none":
398
  return 0, 0
 
 
 
399
  total_ventilation_flow = 0
400
+ num_people = sum(group["num_people"] for group in internal_loads.get("people", []))
401
+ for ventilation in internal_loads.get("ventilation", []):
402
+ space_rate = ventilation.get("design_flow_rate", 0.3) # L/s/m²
403
+ total_ventilation_flow += space_rate * ventilation["area"] / 1000 # m³/s
404
+ if not internal_loads.get("ventilation"):
405
+ total_ventilation_flow = 0.3 * area / 1000 + 2.5 * num_people / 1000 # Default rates
 
 
 
 
 
 
406
  air_density = 1.2
407
  specific_heat = 1000
408
  delta_t = outdoor_temp - indoor_temp
 
416
  return cooling_load, heating_load
417
 
418
  @staticmethod
419
+ def calculate_infiltration_load(internal_loads: Dict, outdoor_temp: float, indoor_temp: float, area: float, building_info: Dict, mode: str = "none") -> tuple[float, float]:
420
  """Calculate infiltration load for heating and cooling in kW based on mode."""
421
  if mode == "none":
422
  return 0, 0
 
 
 
423
  total_infiltration_flow = 0
424
+ building_height = building_info.get("building_height", 3.0)
425
+ volume = area * building_height
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  air_density = 1.2
427
  specific_heat = 1000
428
  delta_t = outdoor_temp - indoor_temp
 
430
  return 0, 0
431
  if mode == "heating" and delta_t >= 0:
432
  return 0, 0
433
+ for infiltration in internal_loads.get("infiltration", []):
434
+ method = infiltration.get("system_type", "AirChanges/Hour")
435
+ if method == "AirChanges/Hour":
436
+ ach = infiltration.get("design_flow_rate", 0.5)
437
+ total_infiltration_flow += ach * volume / 3600
438
+ elif method == "Crack Flow":
439
+ ela = infiltration.get("effective_air_leakage_area", 0.0001) / 10000 # cm² to m²
440
+ wind_speed = 4.0
441
+ total_infiltration_flow += ela * infiltration["area"] * wind_speed / 2
442
+ else: # Flow Equation
443
+ c = infiltration.get("flow_coefficient", 0.0001)
444
+ n = infiltration.get("pressure_exponent", 0.65)
445
+ delta_t_abs = abs(delta_t)
446
+ total_infiltration_flow += c * (delta_t_abs ** n) * infiltration["area"] / 3600
447
+ if not internal_loads.get("infiltration"):
448
+ total_infiltration_flow = 0.5 * volume / 3600 # Default ACH
449
  load = total_infiltration_flow * air_density * specific_heat * delta_t / 1000
450
  cooling_load = load if mode == "cooling" else 0
451
  heating_load = -load if mode == "heating" else 0
 
460
 
461
  @staticmethod
462
  def filter_hourly_data(hourly_data: List[Dict], sim_period: Dict, climate_data: Dict) -> List[Dict]:
463
+ """Filter hourly data based on simulation period, ignoring year."""
464
  if sim_period.get("type") == "Full Year":
465
  return hourly_data
466
  filtered_data = []
467
  if sim_period.get("type") == "From-to":
468
+ start_month = sim_period["start_date"].month
469
+ start_day = sim_period["start_date"].day
470
+ end_month = sim_period["end_date"].month
471
+ end_day = sim_period["end_date"].day
472
  for data in hourly_data:
473
  month, day = data["month"], data["day"]
474
  if (month > start_month or (month == start_month and day >= start_day)) and \
 
484
 
485
  @staticmethod
486
  def get_indoor_conditions(indoor_conditions: Dict, hour: int, outdoor_temp: float, building_info: Dict) -> Dict:
487
+ """Determine indoor conditions based on user settings and building_info."""
488
+ winter_temp = building_info.get("winter_indoor_design_temp", 20.0)
489
+ summer_temp = building_info.get("summer_indoor_design_temp", 24.0)
490
+ winter_rh = building_info.get("winter_indoor_design_rh", 50.0)
491
+ summer_rh = building_info.get("summer_indoor_design_rh", 50.0)
492
+
493
  if indoor_conditions.get("type") == "Fixed":
494
+ mode = "none" if abs(outdoor_temp - winter_temp) < 0.01 else "cooling" if outdoor_temp > summer_temp else "heating"
495
  if mode == "cooling":
496
+ return {"temperature": summer_temp, "rh": summer_rh}
 
 
 
497
  elif mode == "heating":
498
+ return {"temperature": winter_temp, "rh": winter_rh}
 
 
 
499
  else:
500
+ return {"temperature": (winter_temp + summer_temp) / 2, "rh": (winter_rh + summer_rh) / 2}
501
  elif indoor_conditions.get("type") == "Time-varying":
502
  schedule = indoor_conditions.get("schedule", [])
503
  if schedule:
 
505
  for entry in schedule:
506
  if entry["hour"] == hour_idx:
507
  return {"temperature": entry["temperature"], "rh": entry["rh"]}
508
+ return {"temperature": (winter_temp + summer_temp) / 2, "rh": (winter_rh + summer_rh) / 2}
509
  else: # Adaptive
510
+ return {"temperature": TFMCalculations.get_adaptive_comfort_temp(outdoor_temp), "rh": (winter_rh + summer_rh) / 2}
511
 
512
  @staticmethod
513
  def calculate_tfm_loads(components: Dict, hourly_data: List[Dict], indoor_conditions: Dict, internal_loads: Dict, building_info: Dict, sim_period: Dict, hvac_settings: Dict) -> List[Dict]:
514
+ """Calculate TFM loads for heating and cooling with user-defined filters and temperature threshold."""
515
  filtered_data = TFMCalculations.filter_hourly_data(hourly_data, sim_period, building_info)
516
  temp_loads = []
517
  building_orientation = building_info.get("orientation_angle", 0.0)
518
+ operation_hours = building_info.get("operation_hours", 8)
519
+ operating_periods = [{"start": 8, "end": 8 + operation_hours}] # Convert float to dict
520
  area = building_info.get("floor_area", 100.0)
521
+
522
+ if "material_library" not in st.session_state.project_data:
523
+ st.session_state.project_data["material_library"] = MaterialLibrary()
524
+ logger.info("Initialized MaterialLibrary in session_state for solar calculations")
525
 
 
 
 
 
 
526
  for comp_list in components.values():
527
  for comp in comp_list:
528
  comp.ctf = CTFCalculator.calculate_ctf_coefficients(comp)
 
532
  outdoor_temp = hour_data["dry_bulb"]
533
  indoor_cond = TFMCalculations.get_indoor_conditions(indoor_conditions, hour, outdoor_temp, building_info)
534
  indoor_temp = indoor_cond["temperature"]
 
535
  conduction_cooling = conduction_heating = solar = internal = ventilation_cooling = ventilation_heating = infiltration_cooling = infiltration_heating = 0
536
+ is_operating = False
537
+ for period in operating_periods:
538
+ start_hour = period.get("start", 8)
539
+ end_hour = period.get("end", 18)
540
+ if start_hour <= hour % 24 <= end_hour:
541
+ is_operating = True
542
+ break
543
+ mode = "none" if abs(outdoor_temp - building_info.get("winter_indoor_design_temp", 20.0)) < 0.01 else \
544
+ "cooling" if outdoor_temp > building_info.get("summer_indoor_design_temp", 24.0) else "heating"
545
  if is_operating and mode == "cooling":
546
  for comp_list in components.values():
547
  for comp in comp_list:
548
  cool_load, _ = TFMCalculations.calculate_conduction_load(comp, outdoor_temp, indoor_temp, hour, mode="cooling")
549
  conduction_cooling += cool_load
550
  solar += TFMCalculations.calculate_solar_load(comp, hour_data, hour, building_orientation, mode="cooling")
551
+ logger.info(f"Component {comp.name} solar load: {solar:.3f} kW")
552
+ internal = TFMCalculations.calculate_internal_load(internal_loads, hour, operation_hours, area)
553
  ventilation_cooling, _ = TFMCalculations.calculate_ventilation_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, mode="cooling")
554
  infiltration_cooling, _ = TFMCalculations.calculate_infiltration_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, mode="cooling")
555
  elif is_operating and mode == "heating":
 
557
  for comp in comp_list:
558
  _, heat_load = TFMCalculations.calculate_conduction_load(comp, outdoor_temp, indoor_temp, hour, mode="heating")
559
  conduction_heating += heat_load
560
+ internal = TFMCalculations.calculate_internal_load(internal_loads, hour, operation_hours, area)
561
  _, ventilation_heating = TFMCalculations.calculate_ventilation_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, mode="heating")
562
  _, infiltration_heating = TFMCalculations.calculate_infiltration_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, mode="heating")
563
  else:
564
  internal = 0
565
+
566
  logger.info(f"Hour {hour} total loads - conduction: {conduction_cooling:.3f} kW, solar: {solar:.3f} kW, internal: {internal:.3f} kW")
567
 
568
  total_cooling = conduction_cooling + solar + internal + ventilation_cooling + infiltration_cooling
 
571
  total_heating = 0
572
  elif mode == "heating":
573
  total_cooling = 0
 
574
  temp_loads.append({
575
  "hour": hour,
576
  "month": hour_data["month"],
 
586
  "total_cooling": total_cooling,
587
  "total_heating": total_heating
588
  })
589
+
590
  loads_by_day = defaultdict(list)
591
  for load in temp_loads:
592
  day_key = (load["month"], load["day"])
 
607
  return final_loads
608
 
609
  def display_hvac_loads_page():
610
+ """Display the HVAC Loads page and perform calculations."""
611
+ st.header("HVAC Loads")
612
+
613
+ # Access project data
614
  project_data = st.session_state.project_data
615
  components = project_data.get("components", {})
616
  climate_data = project_data.get("climate_data", {})
617
+ hourly_data = climate_data.get("hourly_data", [])
618
  internal_loads = project_data.get("internal_loads", {})
619
  building_info = project_data.get("building_info", {})
620
+
621
+ # Simulation period (assumed to be in climate_data)
622
  sim_period = climate_data.get("typical_extreme_periods", {"type": "Full Year"})
623
+
624
+ # Indoor conditions (modify as needed based on UI inputs)
625
+ indoor_conditions = {
626
+ "type": "Fixed", # Could be "Time-varying" or "Adaptive" based on UI
627
+ "cooling_setpoint": {"temperature": building_info.get("summer_indoor_design_temp", 24.0), "rh": building_info.get("summer_indoor_design_rh", 50.0)},
628
+ "heating_setpoint": {"temperature": building_info.get("winter_indoor_design_temp", 20.0), "rh": building_info.get("winter_indoor_design_rh", 50.0)}
629
+ }
630
+
631
+ # HVAC settings
632
+ hvac_settings = {
633
+ "operating_hours": [{"start": 8, "end": 8 + building_info.get("operation_hours", 8)}]
634
+ }
635
+
636
  if st.button("Calculate HVAC Loads"):
637
+ try:
638
+ loads = TFMCalculations.calculate_tfm_loads(
639
+ components=components,
640
+ hourly_data=hourly_data,
641
+ indoor_conditions=indoor_conditions,
642
+ internal_loads=internal_loads,
643
+ building_info=building_info,
644
+ sim_period=sim_period,
645
+ hvac_settings=hvac_settings
646
+ )
647
+
648
+ # Update project_data
649
+ project_data["hvac_loads"]["cooling"]["hourly"] = [load for load in loads if load["total_cooling"] > 0]
650
+ project_data["hvac_loads"]["heating"]["hourly"] = [load for load in loads if load["total_heating"] > 0]
651
+ project_data["hvac_loads"]["cooling"]["peak"] = max((load["total_cooling"] for load in loads), default=0)
652
+ project_data["hvac_loads"]["heating"]["peak"] = max((load["total_heating"] for load in loads), default=0)
653
+
654
+ # Display results
655
+ st.subheader("Cooling Loads (kW)")
656
+ cooling_df = pd.DataFrame(project_data["hvac_loads"]["cooling"]["hourly"])
657
+ if not cooling_df.empty:
658
+ st.dataframe(cooling_df[["month", "day", "hour", "total_cooling", "conduction_cooling", "solar", "internal", "ventilation_cooling", "infiltration_cooling"]])
659
+
660
+ st.subheader("Heating Loads (kW)")
661
+ heating_df = pd.DataFrame(project_data["hvac_loads"]["heating"]["hourly"])
662
+ if not heating_df.empty:
663
+ st.dataframe(heating_df[["month", "day", "hour", "total_heating", "conduction_heating", "internal", "ventilation_heating", "infiltration_heating"]])
664
+
665
+ st.write(f"Peak Cooling Load: {project_data['hvac_loads']['cooling']['peak']:.2f} kW")
666
+ st.write(f"Peak Heating vanguard: {project_data['hvac_loads']['heating']['peak']:.2f} kW")
667
+
668
+ except Exception as e:
669
+ st.error(f"Error calculating HVAC loads: {str(e)}")
670
+ logger.exception("HVAC load calculation error")
671
 
672
  if __name__ == "__main__":
673
  display_hvac_loads_page()