mabuseif commited on
Commit
d108c6d
verified
1 Parent(s): e588f6e

Update app/hvac_loads.py

Browse files
Files changed (1) hide show
  1. app/hvac_loads.py +106 -201
app/hvac_loads.py CHANGED
@@ -10,7 +10,7 @@ import pandas as pd
10
  from typing import Dict, List, Optional, NamedTuple, Any, Tuple
11
  from enum import Enum
12
  import streamlit as st
13
- from app.material_library import Construction, GlazingMaterial, DoorMaterial, Material, MaterialLibrary
14
  from app.internal_loads import PEOPLE_ACTIVITY_LEVELS, DIVERSITY_FACTORS, LIGHTING_FIXTURE_TYPES, EQUIPMENT_HEAT_GAINS, VENTILATION_RATES, INFILTRATION_SETTINGS
15
  from datetime import datetime
16
  from collections import defaultdict
@@ -23,7 +23,7 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(
23
  logger = logging.getLogger(__name__)
24
 
25
  class TFMCalculations:
26
- # Solar calculation constants (from solar.py)
27
  SHGC_COEFFICIENTS = {
28
  "Single Clear": [0.1, -0.0, 0.0, -0.0, 0.0, 0.87],
29
  "Single Tinted": [0.12, -0.0, 0.0, -0.0, 0.8, -0.0],
@@ -140,51 +140,49 @@ class TFMCalculations:
140
  @staticmethod
141
  def get_surface_parameters(component: Any, building_info: Dict, material_library: MaterialLibrary,
142
  project_materials: Dict, project_constructions: Dict,
143
- project_glazing_materials: Dict, project_door_materials: Dict) -> Tuple[float, float, float, Optional[float], float]:
144
  """
145
- Determine surface parameters (tilt, azimuth, h_o, emissivity, solar_absorption) for a component.
146
- Uses MaterialLibrary to fetch properties from first layer for walls/roofs, DoorMaterial for doors,
147
- and GlazingMaterial for windows/skylights. Handles orientation and tilt based on component type:
148
- - Walls, Doors, Windows: Azimuth = elevation base azimuth + component.rotation; Tilt = 90掳.
149
- - Roofs, Skylights: Azimuth = component.orientation; Tilt = component.tilt (default 180掳).
 
150
 
151
  Args:
152
  component: Component object with component_type, elevation, rotation, orientation, tilt,
153
- construction, glazing_material, or door_material.
154
  building_info (Dict): Building information containing orientation_angle for elevation mapping.
155
  material_library: MaterialLibrary instance for accessing library materials/constructions.
156
  project_materials: Dict of project-specific Material objects.
157
  project_constructions: Dict of project-specific Construction objects.
158
  project_glazing_materials: Dict of project-specific GlazingMaterial objects.
159
- project_door_materials: Dict of project-specific DoorMaterial objects.
160
 
161
  Returns:
162
  Tuple[float, float, float, Optional[float], float]: Surface tilt (掳), surface azimuth (掳),
163
- h_o (W/m虏路K), emissivity, solar_absorption.
164
  """
165
  # Default parameters
166
  component_name = getattr(component, 'name', 'unnamed_component')
167
 
168
  # Initialize default values
169
- surface_tilt = 90.0 # Default vertical for walls, windows, doors
170
  surface_azimuth = 0.0 # Default north-facing
171
  h_o = 17.0 # Default exterior convection coefficient
172
  emissivity = 0.9 # Default for opaque components
173
- solar_absorption = 0.6 # Default
174
 
175
  try:
176
  # Set component-specific defaults based on type
177
  if component.component_type == ComponentType.ROOF:
178
  surface_tilt = getattr(component, 'tilt', 0.0) # Horizontal, upward if tilt absent
179
  h_o = 23.0 # W/m虏路K for roofs
180
- # For roofs, use orientation directly
181
  surface_azimuth = getattr(component, 'orientation', 0.0)
182
  logger.debug(f"Roof component {component_name}: using orientation={surface_azimuth}, tilt={surface_tilt}")
183
 
184
  elif component.component_type == ComponentType.SKYLIGHT:
185
  surface_tilt = getattr(component, 'tilt', 0.0) # Horizontal, upward if tilt absent
186
  h_o = 23.0 # W/m虏路K for skylights
187
- # For skylights, use orientation directly, not elevation
188
  surface_azimuth = getattr(component, 'orientation', 0.0)
189
  logger.debug(f"Skylight component {component_name}: using orientation={surface_azimuth}, tilt={surface_tilt}")
190
 
@@ -194,17 +192,14 @@ class TFMCalculations:
194
  surface_azimuth = 0.0 # Default azimuth for floors
195
  logger.debug(f"Floor component {component_name}: using default azimuth={surface_azimuth}, tilt={surface_tilt}")
196
 
197
- else: # WALL, DOOR, WINDOW
198
  surface_tilt = 90.0 # Vertical
199
  h_o = 17.0 # W/m虏路K
200
-
201
- # Check for elevation attribute
202
  elevation = getattr(component, 'elevation', None)
203
  if not elevation:
204
  logger.warning(f"Component {component_name} ({component.component_type.value}) is missing 'elevation' field. Using default azimuth=0.")
205
- surface_azimuth = 0.0 # Default to north-facing if elevation is missing
206
  else:
207
- # Define elevation azimuths based on building orientation_angle
208
  base_azimuth = building_info.get("orientation_angle", 0.0)
209
  elevation_angles = {
210
  "A": base_azimuth,
@@ -216,9 +211,8 @@ class TFMCalculations:
216
  if elevation not in elevation_angles:
217
  logger.warning(f"Invalid elevation '{elevation}' for component {component_name} ({component.component_type.value}). "
218
  f"Expected one of {list(elevation_angles.keys())}. Using default azimuth=0.")
219
- surface_azimuth = 0.0 # Default to north-facing if elevation is invalid
220
  else:
221
- # Add component rotation to elevation azimuth
222
  surface_azimuth = (elevation_angles[elevation] + getattr(component, 'rotation', 0.0)) % 360
223
  logger.debug(f"Component {component_name} ({component.component_type.value}): elevation={elevation}, "
224
  f"base_azimuth={elevation_angles[elevation]}, rotation={getattr(component, 'rotation', 0.0)}, "
@@ -229,9 +223,8 @@ class TFMCalculations:
229
  construction = getattr(component, 'construction', None)
230
  if not construction:
231
  logger.warning(f"No construction defined for {component_name} ({component.component_type.value}). "
232
- f"Using defaults: solar_absorption=0.6, emissivity=0.9.")
233
  else:
234
- # Get construction from library or project
235
  construction_obj = None
236
  if hasattr(construction, 'name'):
237
  construction_obj = (project_constructions.get(construction.name) or
@@ -239,40 +232,18 @@ class TFMCalculations:
239
 
240
  if not construction_obj:
241
  logger.warning(f"Construction not found for {component_name} ({component.component_type.value}). "
242
- f"Using defaults: solar_absorption=0.6, emissivity=0.9.")
243
  elif not construction_obj.layers:
244
  logger.warning(f"No layers in construction for {component_name} ({component.component_type.value}). "
245
- f"Using defaults: solar_absorption=0.6, emissivity=0.9.")
246
  else:
247
- # Use first (outermost) layer's properties
248
  first_layer = construction_obj.layers[0]
249
  material = first_layer.get("material")
250
  if material:
251
- solar_absorption = getattr(material, 'solar_absorption', 0.6)
252
  emissivity = getattr(material, 'emissivity', 0.9)
253
  logger.debug(f"Using first layer material for {component_name} ({component.component_type.value}): "
254
- f"solar_absorption={solar_absorption}, emissivity={emissivity}")
255
-
256
- elif component.component_type == ComponentType.DOOR:
257
- door_material = getattr(component, 'door_material', None)
258
- if not door_material:
259
- logger.warning(f"No door material defined for {component_name} ({component.component_type.value}). "
260
- f"Using defaults: solar_absorption=0.6, emissivity=0.9.")
261
- else:
262
- # Get door material from library or project
263
- door_material_obj = None
264
- if hasattr(door_material, 'name'):
265
- door_material_obj = (project_door_materials.get(door_material.name) or
266
- material_library.library_door_materials.get(door_material.name))
267
-
268
- if not door_material_obj:
269
- logger.warning(f"Door material not found for {component_name} ({component.component_type.value}). "
270
- f"Using defaults: solar_absorption=0.6, emissivity=0.9.")
271
- else:
272
- solar_absorption = getattr(door_material_obj, 'solar_absorption', 0.6)
273
- emissivity = getattr(door_material_obj, 'emissivity', 0.9)
274
- logger.debug(f"Using door material for {component_name} ({component.component_type.value}): "
275
- f"solar_absorption={solar_absorption}, emissivity={emissivity}")
276
 
277
  elif component.component_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]:
278
  glazing_material = getattr(component, 'glazing_material', None)
@@ -281,7 +252,6 @@ class TFMCalculations:
281
  f"Using default SHGC=0.7, h_o={h_o}.")
282
  shgc = 0.7
283
  else:
284
- # Get glazing material from library or project
285
  glazing_material_obj = None
286
  if hasattr(glazing_material, 'name'):
287
  glazing_material_obj = (project_glazing_materials.get(glazing_material.name) or
@@ -296,41 +266,37 @@ class TFMCalculations:
296
  h_o = getattr(glazing_material_obj, 'h_o', h_o)
297
  logger.debug(f"Using glazing material for {component_name} ({component.component_type.value}): "
298
  f"shgc={shgc}, h_o={h_o}")
299
- emissivity = None # Not used for glazing
300
 
301
  except Exception as e:
302
  logger.error(f"Error retrieving surface parameters for {component_name} ({component.component_type.value}): {str(e)}")
303
- # Apply defaults based on component type
304
  if component.component_type == ComponentType.ROOF:
305
- surface_tilt = 0.0 # Horizontal, upward
306
- h_o = 23.0 # W/m虏路K for roofs
307
- surface_azimuth = 0.0 # Default north
308
  elif component.component_type == ComponentType.SKYLIGHT:
309
- surface_tilt = 0.0 # Horizontal, upward
310
- h_o = 23.0 # W/m虏路K for skylights
311
- surface_azimuth = 0.0 # Default north
312
  elif component.component_type == ComponentType.FLOOR:
313
- surface_tilt = 180.0 # Horizontal, downward
314
- h_o = 17.0 # W/m虏路K
315
- surface_azimuth = 0.0 # Default north
316
- else: # WALL, DOOR, WINDOW
317
- surface_tilt = 90.0 # Vertical
318
- h_o = 17.0 # W/m虏路K
319
- surface_azimuth = 0.0 # Default north
320
 
321
- # Apply material defaults
322
- if component.component_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.DOOR]:
323
- solar_absorption = 0.6
324
  emissivity = 0.9
325
  else: # WINDOW, SKYLIGHT
326
  shgc = 0.7
327
  emissivity = None
328
 
329
- # Debug output for all components
330
  logger.info(f"Final surface parameters for {component_name} ({component.component_type.value}): "
331
  f"tilt={surface_tilt:.1f}, azimuth={surface_azimuth:.1f}, h_o={h_o:.1f}")
332
-
333
- return surface_tilt, surface_azimuth, h_o, emissivity, solar_absorption
334
 
335
  @staticmethod
336
  def calculate_solar_load(component, hourly_data: Dict, hour: int, building_orientation: float, mode: str = "none") -> float:
@@ -349,42 +315,31 @@ class TFMCalculations:
349
  References:
350
  ASHRAE Handbook鈥擣undamentals, Chapters 15 and 18.
351
  """
352
- # Only calculate solar loads in cooling mode
353
  if mode != "cooling":
354
  return 0
355
 
356
- # Skip floors for solar calculation
357
  if component.component_type == ComponentType.FLOOR:
358
  return 0
359
 
360
  component_name = getattr(component, 'name', 'unnamed_component')
361
 
362
  try:
363
- # Ensure MaterialLibrary is properly initialized and accessible
364
  material_library = st.session_state.get("material_library")
365
  if not material_library:
366
- logger.error(f"MaterialLibrary not found in session_state for {component_name} ({component.component_type.value})")
367
- # Instead of raising an error, initialize a new MaterialLibrary
368
- from data.material_library import MaterialLibrary
369
  material_library = MaterialLibrary()
370
  st.session_state.material_library = material_library
371
  logger.info(f"Created new MaterialLibrary for {component_name} ({component.component_type.value})")
372
 
373
- project_materials = st.session_state.get("project_materials", {})
374
- project_constructions = st.session_state.get("project_constructions", {})
375
- project_glazing_materials = st.session_state.get("project_glazing_materials", {})
376
- project_door_materials = st.session_state.get("project_door_materials", {})
377
-
378
- # Get location parameters from climate_data
379
- climate_data = st.session_state.get("climate_data", {})
380
  latitude = climate_data.get("latitude", 0.0)
381
  longitude = climate_data.get("longitude", 0.0)
382
  timezone = climate_data.get("time_zone", 0.0)
 
383
 
384
- # Get ground reflectivity (default 0.2)
385
- ground_reflectivity = st.session_state.get("ground_reflectivity", 0.2)
386
-
387
- # Validate input parameters
388
  if not -90 <= latitude <= 90:
389
  logger.warning(f"Invalid latitude {latitude} for {component_name} ({component.component_type.value}). Using default 0.0.")
390
  latitude = 0.0
@@ -398,140 +353,115 @@ class TFMCalculations:
398
  logger.warning(f"Invalid ground_reflectivity {ground_reflectivity} for {component_name} ({component.component_type.value}). Using default 0.2.")
399
  ground_reflectivity = 0.2
400
 
401
- # Ensure hourly_data has required fields
402
  required_fields = ["month", "day", "hour", "global_horizontal_radiation", "direct_normal_radiation",
403
  "diffuse_horizontal_radiation", "dry_bulb"]
404
  if not all(field in hourly_data for field in required_fields):
405
  logger.warning(f"Missing required fields in hourly_data for hour {hour} for {component_name} ({component.component_type.value}): {hourly_data}")
406
  return 0
407
 
408
- # Skip if GHI <= 0
409
  if hourly_data["global_horizontal_radiation"] <= 0:
410
  logger.info(f"No solar load for hour {hour} due to GHI={hourly_data['global_horizontal_radiation']} for {component_name} ({component.component_type.value})")
411
  return 0
412
 
413
- # Extract weather data
414
  month = hourly_data["month"]
415
  day = hourly_data["day"]
416
  hour = hourly_data["hour"]
417
  ghi = hourly_data["global_horizontal_radiation"]
418
- dni = hourly_data.get("direct_normal_radiation", ghi * 0.7) # Fallback: estimate DNI
419
- dhi = hourly_data.get("diffuse_horizontal_radiation", ghi * 0.3) # Fallback: estimate DHI
420
  outdoor_temp = hourly_data["dry_bulb"]
421
 
422
  if ghi < 0 or dni < 0 or dhi < 0:
423
  logger.error(f"Negative radiation values for {month}/{day}/{hour} for {component_name} ({component.component_type.value})")
424
  raise ValueError(f"Negative radiation values for {month}/{day}/{hour}")
425
 
426
- # Add detailed logging for solar calculation
427
  logger.info(f"Processing solar for {month}/{day}/{hour} with GHI={ghi}, DNI={dni}, DHI={dhi}, "
428
  f"dry_bulb={outdoor_temp} for {component_name} ({component.component_type.value})")
429
 
430
- # Step 1: Local Solar Time (LST) with Equation of Time
431
- year = 2025 # Fixed year since not provided
432
  n = TFMCalculations.day_of_year(month, day, year)
433
  EOT = TFMCalculations.equation_of_time(n)
434
- lambda_std = 15 * timezone # Standard meridian longitude (掳)
435
- standard_time = hour - 1 + 0.5 # Convert to decimal, assume mid-hour
436
  LST = standard_time + (4 * (lambda_std - longitude) + EOT) / 60
437
 
438
- # Step 2: Solar Declination (未)
439
  delta = 23.45 * math.sin(math.radians(360 / 365 * (284 + n)))
440
-
441
- # Step 3: Hour Angle (HRA)
442
- hra = 15 * (LST - 12)
443
-
444
- # Step 4: Solar Altitude (伪) and Azimuth (蠄)
445
  phi = math.radians(latitude)
446
  delta_rad = math.radians(delta)
 
447
  hra_rad = math.radians(hra)
448
 
449
  sin_alpha = math.sin(phi) * math.sin(delta_rad) + math.cos(phi) * math.cos(delta_rad) * math.cos(hra_rad)
450
  alpha = math.degrees(math.asin(sin_alpha))
451
 
452
  if abs(math.cos(math.radians(alpha))) < 0.01:
453
- azimuth = 0 # North at sunrise/sunset
454
  else:
455
  sin_az = math.cos(delta_rad) * math.sin(hra_rad) / math.cos(math.radians(alpha))
456
  cos_az = (sin_alpha * math.sin(phi) - math.sin(delta_rad)) / (math.cos(math.radians(alpha)) * math.cos(phi))
457
  azimuth = math.degrees(math.atan2(sin_az, cos_az))
458
- if hra > 0: # Afternoon
459
  azimuth = 360 - azimuth if azimuth > 0 else -azimuth
460
 
461
  logger.info(f"Solar angles for {month}/{day}/{hour}: declination={delta:.2f}, LST={LST:.2f}, "
462
  f"HRA={hra:.2f}, altitude={alpha:.2f}, azimuth={azimuth:.2f} for {component_name} ({component.component_type.value})")
463
 
464
- # Step 5: Get surface parameters with robust error handling
465
  building_info = {"orientation_angle": building_orientation}
466
  try:
467
- surface_tilt, surface_azimuth, h_o, emissivity, solar_absorption = \
468
  TFMCalculations.get_surface_parameters(
469
  component, building_info, material_library, project_materials,
470
- project_constructions, project_glazing_materials, project_door_materials
471
  )
472
  except Exception as e:
473
  logger.error(f"Error getting surface parameters for {component_name}: {str(e)}. Using defaults.")
474
- # Apply defaults based on component type
475
  if component.component_type == ComponentType.ROOF:
476
- surface_tilt = 0.0 # Horizontal, upward
477
- surface_azimuth = 0.0 # Default north
478
  elif component.component_type == ComponentType.SKYLIGHT:
479
- surface_tilt = 0.0 # Horizontal, upward
480
- surface_azimuth = 0.0 # Default north
481
  elif component.component_type == ComponentType.FLOOR:
482
- surface_tilt = 180.0 # Horizontal, downward
483
- surface_azimuth = 0.0 # Default north
484
- else: # WALL, DOOR, WINDOW
485
- surface_tilt = 90.0 # Vertical
486
- surface_azimuth = 0.0 # Default north
487
 
488
- # Apply material defaults
489
- if component.component_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.DOOR]:
490
- solar_absorption = 0.6
491
  h_o = 17.0 if component.component_type == ComponentType.WALL else 23.0
492
  else: # WINDOW, SKYLIGHT
493
- solar_absorption = 0.0 # Not used for glazing
494
  h_o = 17.0 if component.component_type == ComponentType.WINDOW else 23.0
495
 
496
- # Step 6: Calculate angle of incidence (胃)
497
- # Convert angles to radians for calculation
498
  alpha_rad = math.radians(alpha)
499
  surface_tilt_rad = math.radians(surface_tilt)
500
  azimuth_rad = math.radians(azimuth)
501
  surface_azimuth_rad = math.radians(surface_azimuth)
502
 
503
- # Calculate cos(胃) using the solar position and surface orientation
504
  cos_theta = (math.sin(alpha_rad) * math.cos(surface_tilt_rad) +
505
  math.cos(alpha_rad) * math.sin(surface_tilt_rad) *
506
  math.cos(azimuth_rad - surface_azimuth_rad))
507
 
508
- # Clamp to [0, 1] to avoid numerical issues
509
  cos_theta = max(min(cos_theta, 1.0), 0.0)
510
 
511
- # Log the calculated values
512
  logger.info(f" Component {component_name} ({component.component_type.value}) at {month}/{day}/{hour}: "
513
  f"surface_tilt={surface_tilt:.2f}, surface_azimuth={surface_azimuth:.2f}, "
514
  f"cos_theta={cos_theta:.4f}")
515
 
516
- # Step 7: Calculate total incident radiation (I_t)
517
- # Calculate view factor for ground-reflected radiation
518
  view_factor = (1 - math.cos(surface_tilt_rad)) / 2
519
-
520
- # Calculate ground-reflected radiation
521
  ground_reflected = ground_reflectivity * ghi * view_factor
522
 
523
- # Calculate total incident radiation
524
- if cos_theta > 0: # Surface receives direct beam radiation
525
  I_t = dni * cos_theta + dhi + ground_reflected
526
- else: # Surface in shade, only diffuse and reflected
527
  I_t = dhi + ground_reflected
528
 
529
- # Step 8: Calculate solar heat gain based on component type
530
  solar_heat_gain = 0.0
531
 
532
  if component.component_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]:
533
- # For windows/skylights, get SHGC from material
534
- shgc = 0.7 # Default
535
  glazing_material = getattr(component, 'glazing_material', None)
536
  if glazing_material:
537
  glazing_material_obj = None
@@ -544,36 +474,28 @@ class TFMCalculations:
544
  h_o = getattr(glazing_material_obj, 'h_o', h_o)
545
  else:
546
  logger.warning(f"Glazing material not found for {component_name} ({component.component_type.value}). Using default SHGC=0.7.")
547
- else:
548
- logger.warning(f"No glazing material defined for {component_name} ({component.component_type.value}). Using default SHGC=0.7.")
549
 
550
- # Get glazing type for dynamic SHGC calculation
551
- glazing_type = "Single Clear" # Default
552
  if hasattr(component, 'name') and component.name in TFMCalculations.GLAZING_TYPE_MAPPING:
553
  glazing_type = TFMCalculations.GLAZING_TYPE_MAPPING[component.name]
554
 
555
- # Get internal shading coefficient
556
- iac = getattr(component, 'iac', 1.0) # Default internal shading
557
 
558
- # Calculate dynamic SHGC based on incidence angle
559
  shgc_dynamic = shgc * TFMCalculations.calculate_dynamic_shgc(glazing_type, cos_theta)
560
 
561
- # Calculate solar heat gain for fenestration
562
- solar_heat_gain = component.area * shgc_dynamic * I_t * iac / 1000 # kW
563
 
564
  logger.info(f"Fenestration solar heat gain for {component_name} ({component.component_type.value}) at {month}/{day}/{hour}: "
565
  f"{solar_heat_gain:.4f} kW (area={component.area}, shgc_dynamic={shgc_dynamic:.4f}, "
566
  f"I_t={I_t:.2f}, iac={iac})")
567
 
568
- elif component.component_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.DOOR]:
569
- # For opaque surfaces, use solar absorptivity and surface resistance
570
- surface_resistance = 1/h_o # m虏路K/W
571
 
572
- # Calculate absorbed solar radiation
573
- solar_heat_gain = component.area * solar_absorption * I_t * surface_resistance / 1000 # kW
574
 
575
  logger.info(f"Opaque surface solar heat gain for {component_name} ({component.component_type.value}) at {month}/{day}/{hour}: "
576
- f"{solar_heat_gain:.4f} kW (area={component.area}, solar_absorption={solar_absorption:.2f}, "
577
  f"I_t={I_t:.2f}, surface_resistance={surface_resistance:.4f})")
578
 
579
  return solar_heat_gain
@@ -598,11 +520,10 @@ class TFMCalculations:
598
  fraction = min(lighting_operating_hours, operation_hours) / operation_hours if operation_hours > 0 else 0
599
  lighting_load = lpd * area * fraction
600
  total_load += lighting_load
601
- equipment = internal_loads.get("equipment")
602
- if equipment:
603
- total_power_density = equipment.get("total_power_density", 0)
604
- equipment_load = total_power_density * area
605
- total_load += equipment_load
606
  return total_load / 1000
607
 
608
  @staticmethod
@@ -610,21 +531,21 @@ class TFMCalculations:
610
  """Calculate ventilation load for heating and cooling in kW based on mode."""
611
  if mode == "none":
612
  return 0, 0
613
- ventilation = internal_loads.get("ventilation")
614
  if not ventilation:
615
  return 0, 0
616
- space_rate = ventilation.get("space_rate", 0.3) # L/s/m虏
617
- people_rate = ventilation.get("people_rate", 2.5) # L/s/person
618
  num_people = sum(group["num_people"] for group in internal_loads.get("people", []))
619
- ventilation_flow = (space_rate * area + people_rate * num_people) / 1000 # m鲁/s
620
- air_density = 1.2 # kg/m鲁
621
- specific_heat = 1000 # J/kg路K
622
  delta_t = outdoor_temp - indoor_temp
623
  if mode == "cooling" and delta_t <= 0:
624
  return 0, 0
625
  if mode == "heating" and delta_t >= 0:
626
  return 0, 0
627
- load = ventilation_flow * air_density * specific_heat * delta_t / 1000 # kW
628
  cooling_load = load if mode == "cooling" else 0
629
  heating_load = -load if mode == "heating" else 0
630
  return cooling_load, heating_load
@@ -634,15 +555,15 @@ class TFMCalculations:
634
  """Calculate infiltration load for heating and cooling in kW based on mode."""
635
  if mode == "none":
636
  return 0, 0
637
- infiltration = internal_loads.get("infiltration")
638
  if not infiltration:
639
  return 0, 0
640
  method = infiltration.get("method", "ACH")
641
  settings = infiltration.get("settings", {})
642
  building_height = building_info.get("building_height", 3.0)
643
- volume = area * building_height # m鲁
644
- air_density = 1.2 # kg/m鲁
645
- specific_heat = 1000 # J/kg路K
646
  delta_t = outdoor_temp - indoor_temp
647
  if mode == "cooling" and delta_t <= 0:
648
  return 0, 0
@@ -650,17 +571,17 @@ class TFMCalculations:
650
  return 0, 0
651
  if method == "ACH":
652
  ach = settings.get("rate", 0.5)
653
- infiltration_flow = ach * volume / 3600 # m鲁/s
654
  elif method == "Crack Flow":
655
- ela = settings.get("ela", 0.0001) # m虏/m虏
656
- wind_speed = 4.0 # m/s (assumed)
657
- infiltration_flow = ela * area * wind_speed / 2 # m鲁/s
658
  else: # Empirical Equations
659
  c = settings.get("c", 0.1)
660
  n = settings.get("n", 0.65)
661
  delta_t_abs = abs(delta_t)
662
- infiltration_flow = c * (delta_t_abs ** n) * area / 3600 # m鲁/s
663
- load = infiltration_flow * air_density * specific_heat * delta_t / 1000 # kW
664
  cooling_load = load if mode == "cooling" else 0
665
  heating_load = -load if mode == "heating" else 0
666
  return cooling_load, heating_load
@@ -670,7 +591,7 @@ class TFMCalculations:
670
  """Calculate adaptive comfort temperature per ASHRAE 55."""
671
  if 10 <= outdoor_temp <= 33.5:
672
  return 0.31 * outdoor_temp + 17.8
673
- return 24.0 # Default to standard setpoint if outside range
674
 
675
  @staticmethod
676
  def filter_hourly_data(hourly_data: List[Dict], sim_period: Dict, climate_data: Dict) -> List[Dict]:
@@ -708,7 +629,7 @@ class TFMCalculations:
708
  }
709
  elif mode == "heating":
710
  return {
711
- "temperature": indoor_conditions.get("heating_setpoint", {}).get("temperature", 22.0),
712
  "rh": indoor_conditions.get("heating_setpoint", {}).get("rh", 50.0)
713
  }
714
  else:
@@ -733,13 +654,11 @@ class TFMCalculations:
733
  operating_periods = hvac_settings.get("operating_hours", [{"start": 8, "end": 18}])
734
  area = building_info.get("floor_area", 100.0)
735
 
736
- # Ensure MaterialLibrary is properly initialized
737
  if "material_library" not in st.session_state:
738
- from data.material_library import MaterialLibrary
739
  st.session_state.material_library = MaterialLibrary()
740
  logger.info("Initialized MaterialLibrary in session_state for solar calculations")
741
 
742
- # Pre-calculate CTF coefficients for all components using CTFCalculator
743
  for comp_list in components.values():
744
  for comp in comp_list:
745
  comp.ctf = CTFCalculator.calculate_ctf_coefficients(comp)
@@ -749,9 +668,7 @@ class TFMCalculations:
749
  outdoor_temp = hour_data["dry_bulb"]
750
  indoor_cond = TFMCalculations.get_indoor_conditions(indoor_conditions, hour, outdoor_temp)
751
  indoor_temp = indoor_cond["temperature"]
752
- # Initialize all loads to 0
753
  conduction_cooling = conduction_heating = solar = internal = ventilation_cooling = ventilation_heating = infiltration_cooling = infiltration_heating = 0
754
- # Check if hour is within operating periods
755
  is_operating = False
756
  for period in operating_periods:
757
  start_hour = period.get("start", 8)
@@ -759,20 +676,14 @@ class TFMCalculations:
759
  if start_hour <= hour % 24 <= end_hour:
760
  is_operating = True
761
  break
762
- # Determine mode based on temperature threshold (18掳C)
763
  mode = "none" if abs(outdoor_temp - 18) < 0.01 else "cooling" if outdoor_temp > 18 else "heating"
764
  if is_operating and mode == "cooling":
765
- # Calculate solar load for each component and accumulate
766
  for comp_list in components.values():
767
  for comp in comp_list:
768
  cool_load, _ = TFMCalculations.calculate_conduction_load(comp, outdoor_temp, indoor_temp, hour, mode="cooling")
769
  conduction_cooling += cool_load
770
-
771
- # Calculate solar load for each component and accumulate
772
  component_solar_load = TFMCalculations.calculate_solar_load(comp, hour_data, hour, building_orientation, mode="cooling")
773
  solar += component_solar_load
774
-
775
- # Add detailed logging for solar load accumulation
776
  logger.info(f"Component {comp.name} ({comp.component_type.value}) solar load: {component_solar_load:.3f} kW, accumulated solar: {solar:.3f} kW")
777
 
778
  internal = TFMCalculations.calculate_internal_load(internal_loads, hour, max([p["end"] - p["start"] for p in operating_periods]), area)
@@ -786,16 +697,13 @@ class TFMCalculations:
786
  internal = TFMCalculations.calculate_internal_load(internal_loads, hour, max([p["end"] - p["start"] for p in operating_periods]), area)
787
  _, ventilation_heating = TFMCalculations.calculate_ventilation_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, mode="heating")
788
  _, infiltration_heating = TFMCalculations.calculate_infiltration_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, mode="heating")
789
- else: # mode == "none" or not is_operating
790
- internal = 0 # No internal loads when no heating or cooling is needed or outside operating hours
791
 
792
- # Add detailed logging for total loads
793
  logger.info(f"Hour {hour} total loads - conduction: {conduction_cooling:.3f} kW, solar: {solar:.3f} kW, internal: {internal:.3f} kW")
794
 
795
- # Calculate total loads, subtracting internal load for heating
796
  total_cooling = conduction_cooling + solar + internal + ventilation_cooling + infiltration_cooling
797
  total_heating = max(conduction_heating + ventilation_heating + infiltration_heating - internal, 0)
798
- # Enforce mutual exclusivity within hour
799
  if mode == "cooling":
800
  total_heating = 0
801
  elif mode == "heating":
@@ -815,24 +723,21 @@ class TFMCalculations:
815
  "total_cooling": total_cooling,
816
  "total_heating": total_heating
817
  })
818
- # Group loads by day and apply daily control
819
  loads_by_day = defaultdict(list)
820
  for load in temp_loads:
821
  day_key = (load["month"], load["day"])
822
  loads_by_day[day_key].append(load)
823
  final_loads = []
824
  for day_key, day_loads in loads_by_day.items():
825
- # Count hours with non-zero cooling and heating loads
826
  cooling_hours = sum(1 for load in day_loads if load["total_cooling"] > 0)
827
  heating_hours = sum(1 for load in day_loads if load["total_heating"] > 0)
828
- # Apply daily control
829
  for load in day_loads:
830
  if cooling_hours > heating_hours:
831
- load["total_heating"] = 0 # Keep cooling components, zero heating total
832
  elif heating_hours > cooling_hours:
833
- load["total_cooling"] = 0 # Keep heating components, zero cooling total
834
- else: # Equal hours
835
  load["total_cooling"] = 0
836
- load["total_heating"] = 0 # Zero both totals, keep components
 
 
837
  final_loads.append(load)
838
  return final_loads
 
10
  from typing import Dict, List, Optional, NamedTuple, Any, Tuple
11
  from enum import Enum
12
  import streamlit as st
13
+ from app.material_library import Construction, GlazingMaterial, Material, MaterialLibrary
14
  from app.internal_loads import PEOPLE_ACTIVITY_LEVELS, DIVERSITY_FACTORS, LIGHTING_FIXTURE_TYPES, EQUIPMENT_HEAT_GAINS, VENTILATION_RATES, INFILTRATION_SETTINGS
15
  from datetime import datetime
16
  from collections import defaultdict
 
23
  logger = logging.getLogger(__name__)
24
 
25
  class TFMCalculations:
26
+ # Solar calculation constants (from utils/solar.py)
27
  SHGC_COEFFICIENTS = {
28
  "Single Clear": [0.1, -0.0, 0.0, -0.0, 0.0, 0.87],
29
  "Single Tinted": [0.12, -0.0, 0.0, -0.0, 0.8, -0.0],
 
140
  @staticmethod
141
  def get_surface_parameters(component: Any, building_info: Dict, material_library: MaterialLibrary,
142
  project_materials: Dict, project_constructions: Dict,
143
+ project_glazing_materials: Dict) -> Tuple[float, float, float, Optional[float], float]:
144
  """
145
+ Determine surface parameters (tilt, azimuth, h_o, emissivity, absorptivity) for a component.
146
+ Uses MaterialLibrary to fetch properties from first layer for walls/roofs, and GlazingMaterial for windows/skylights.
147
+ Handles orientation and tilt based on component type:
148
+ - Walls, Windows: Azimuth = elevation base azimuth + component.rotation; Tilt = 90掳.
149
+ - Roofs, Skylights: Azimuth = component.orientation; Tilt = component.tilt (default 0掳).
150
+ - Floors: Tilt = 180掳; Azimuth = 0掳.
151
 
152
  Args:
153
  component: Component object with component_type, elevation, rotation, orientation, tilt,
154
+ construction, or glazing_material.
155
  building_info (Dict): Building information containing orientation_angle for elevation mapping.
156
  material_library: MaterialLibrary instance for accessing library materials/constructions.
157
  project_materials: Dict of project-specific Material objects.
158
  project_constructions: Dict of project-specific Construction objects.
159
  project_glazing_materials: Dict of project-specific GlazingMaterial objects.
 
160
 
161
  Returns:
162
  Tuple[float, float, float, Optional[float], float]: Surface tilt (掳), surface azimuth (掳),
163
+ h_o (W/m虏路K), emissivity, absorptivity.
164
  """
165
  # Default parameters
166
  component_name = getattr(component, 'name', 'unnamed_component')
167
 
168
  # Initialize default values
169
+ surface_tilt = 90.0 # Default vertical for walls, windows
170
  surface_azimuth = 0.0 # Default north-facing
171
  h_o = 17.0 # Default exterior convection coefficient
172
  emissivity = 0.9 # Default for opaque components
173
+ absorptivity = 0.6 # Default
174
 
175
  try:
176
  # Set component-specific defaults based on type
177
  if component.component_type == ComponentType.ROOF:
178
  surface_tilt = getattr(component, 'tilt', 0.0) # Horizontal, upward if tilt absent
179
  h_o = 23.0 # W/m虏路K for roofs
 
180
  surface_azimuth = getattr(component, 'orientation', 0.0)
181
  logger.debug(f"Roof component {component_name}: using orientation={surface_azimuth}, tilt={surface_tilt}")
182
 
183
  elif component.component_type == ComponentType.SKYLIGHT:
184
  surface_tilt = getattr(component, 'tilt', 0.0) # Horizontal, upward if tilt absent
185
  h_o = 23.0 # W/m虏路K for skylights
 
186
  surface_azimuth = getattr(component, 'orientation', 0.0)
187
  logger.debug(f"Skylight component {component_name}: using orientation={surface_azimuth}, tilt={surface_tilt}")
188
 
 
192
  surface_azimuth = 0.0 # Default azimuth for floors
193
  logger.debug(f"Floor component {component_name}: using default azimuth={surface_azimuth}, tilt={surface_tilt}")
194
 
195
+ else: # WALL, WINDOW
196
  surface_tilt = 90.0 # Vertical
197
  h_o = 17.0 # W/m虏路K
 
 
198
  elevation = getattr(component, 'elevation', None)
199
  if not elevation:
200
  logger.warning(f"Component {component_name} ({component.component_type.value}) is missing 'elevation' field. Using default azimuth=0.")
201
+ surface_azimuth = 0.0
202
  else:
 
203
  base_azimuth = building_info.get("orientation_angle", 0.0)
204
  elevation_angles = {
205
  "A": base_azimuth,
 
211
  if elevation not in elevation_angles:
212
  logger.warning(f"Invalid elevation '{elevation}' for component {component_name} ({component.component_type.value}). "
213
  f"Expected one of {list(elevation_angles.keys())}. Using default azimuth=0.")
214
+ surface_azimuth = 0.0
215
  else:
 
216
  surface_azimuth = (elevation_angles[elevation] + getattr(component, 'rotation', 0.0)) % 360
217
  logger.debug(f"Component {component_name} ({component.component_type.value}): elevation={elevation}, "
218
  f"base_azimuth={elevation_angles[elevation]}, rotation={getattr(component, 'rotation', 0.0)}, "
 
223
  construction = getattr(component, 'construction', None)
224
  if not construction:
225
  logger.warning(f"No construction defined for {component_name} ({component.component_type.value}). "
226
+ f"Using defaults: absorptivity=0.6, emissivity=0.9.")
227
  else:
 
228
  construction_obj = None
229
  if hasattr(construction, 'name'):
230
  construction_obj = (project_constructions.get(construction.name) or
 
232
 
233
  if not construction_obj:
234
  logger.warning(f"Construction not found for {component_name} ({component.component_type.value}). "
235
+ f"Using defaults: absorptivity=0.6, emissivity=0.9.")
236
  elif not construction_obj.layers:
237
  logger.warning(f"No layers in construction for {component_name} ({component.component_type.value}). "
238
+ f"Using defaults: absorptivity=0.6, emissivity=0.9.")
239
  else:
 
240
  first_layer = construction_obj.layers[0]
241
  material = first_layer.get("material")
242
  if material:
243
+ absorptivity = getattr(material, 'absorptivity', 0.6)
244
  emissivity = getattr(material, 'emissivity', 0.9)
245
  logger.debug(f"Using first layer material for {component_name} ({component.component_type.value}): "
246
+ f"absorptivity={absorptivity}, emissivity={emissivity}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
 
248
  elif component.component_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]:
249
  glazing_material = getattr(component, 'glazing_material', None)
 
252
  f"Using default SHGC=0.7, h_o={h_o}.")
253
  shgc = 0.7
254
  else:
 
255
  glazing_material_obj = None
256
  if hasattr(glazing_material, 'name'):
257
  glazing_material_obj = (project_glazing_materials.get(glazing_material.name) or
 
266
  h_o = getattr(glazing_material_obj, 'h_o', h_o)
267
  logger.debug(f"Using glazing material for {component_name} ({component.component_type.value}): "
268
  f"shgc={shgc}, h_o={h_o}")
269
+ emissivity = None
270
 
271
  except Exception as e:
272
  logger.error(f"Error retrieving surface parameters for {component_name} ({component.component_type.value}): {str(e)}")
 
273
  if component.component_type == ComponentType.ROOF:
274
+ surface_tilt = 0.0
275
+ h_o = 23.0
276
+ surface_azimuth = 0.0
277
  elif component.component_type == ComponentType.SKYLIGHT:
278
+ surface_tilt = 0.0
279
+ h_o = 23.0
280
+ surface_azimuth = 0.0
281
  elif component.component_type == ComponentType.FLOOR:
282
+ surface_tilt = 180.0
283
+ h_o = 17.0
284
+ surface_azimuth = 0.0
285
+ else: # WALL, WINDOW
286
+ surface_tilt = 90.0
287
+ h_o = 17.0
288
+ surface_azimuth = 0.0
289
 
290
+ if component.component_type in [ComponentType.WALL, ComponentType.ROOF]:
291
+ absorptivity = 0.6
 
292
  emissivity = 0.9
293
  else: # WINDOW, SKYLIGHT
294
  shgc = 0.7
295
  emissivity = None
296
 
 
297
  logger.info(f"Final surface parameters for {component_name} ({component.component_type.value}): "
298
  f"tilt={surface_tilt:.1f}, azimuth={surface_azimuth:.1f}, h_o={h_o:.1f}")
299
+ return surface_tilt, surface_azimuth, h_o, emissivity, absorptivity
 
300
 
301
  @staticmethod
302
  def calculate_solar_load(component, hourly_data: Dict, hour: int, building_orientation: float, mode: str = "none") -> float:
 
315
  References:
316
  ASHRAE Handbook鈥擣undamentals, Chapters 15 and 18.
317
  """
 
318
  if mode != "cooling":
319
  return 0
320
 
 
321
  if component.component_type == ComponentType.FLOOR:
322
  return 0
323
 
324
  component_name = getattr(component, 'name', 'unnamed_component')
325
 
326
  try:
 
327
  material_library = st.session_state.get("material_library")
328
  if not material_library:
329
+ from app.material_library import MaterialLibrary
 
 
330
  material_library = MaterialLibrary()
331
  st.session_state.material_library = material_library
332
  logger.info(f"Created new MaterialLibrary for {component_name} ({component.component_type.value})")
333
 
334
+ project_materials = st.session_state.get("project_data", {}).get("materials", {}).get("project", {})
335
+ project_constructions = st.session_state.get("project_data", {}).get("constructions", {}).get("project", {})
336
+ project_glazing_materials = st.session_state.get("project_data", {}).get("fenestrations", {}).get("project", {})
337
+ climate_data = st.session_state.get("project_data", {}).get("climate_data", {})
 
 
 
338
  latitude = climate_data.get("latitude", 0.0)
339
  longitude = climate_data.get("longitude", 0.0)
340
  timezone = climate_data.get("time_zone", 0.0)
341
+ ground_reflectivity = st.session_state.get("project_data", {}).get("climate_data", {}).get("ground_reflectivity", 0.2)
342
 
 
 
 
 
343
  if not -90 <= latitude <= 90:
344
  logger.warning(f"Invalid latitude {latitude} for {component_name} ({component.component_type.value}). Using default 0.0.")
345
  latitude = 0.0
 
353
  logger.warning(f"Invalid ground_reflectivity {ground_reflectivity} for {component_name} ({component.component_type.value}). Using default 0.2.")
354
  ground_reflectivity = 0.2
355
 
 
356
  required_fields = ["month", "day", "hour", "global_horizontal_radiation", "direct_normal_radiation",
357
  "diffuse_horizontal_radiation", "dry_bulb"]
358
  if not all(field in hourly_data for field in required_fields):
359
  logger.warning(f"Missing required fields in hourly_data for hour {hour} for {component_name} ({component.component_type.value}): {hourly_data}")
360
  return 0
361
 
 
362
  if hourly_data["global_horizontal_radiation"] <= 0:
363
  logger.info(f"No solar load for hour {hour} due to GHI={hourly_data['global_horizontal_radiation']} for {component_name} ({component.component_type.value})")
364
  return 0
365
 
 
366
  month = hourly_data["month"]
367
  day = hourly_data["day"]
368
  hour = hourly_data["hour"]
369
  ghi = hourly_data["global_horizontal_radiation"]
370
+ dni = hourly_data.get("direct_normal_radiation", ghi * 0.7)
371
+ dhi = hourly_data.get("diffuse_horizontal_radiation", ghi * 0.3)
372
  outdoor_temp = hourly_data["dry_bulb"]
373
 
374
  if ghi < 0 or dni < 0 or dhi < 0:
375
  logger.error(f"Negative radiation values for {month}/{day}/{hour} for {component_name} ({component.component_type.value})")
376
  raise ValueError(f"Negative radiation values for {month}/{day}/{hour}")
377
 
 
378
  logger.info(f"Processing solar for {month}/{day}/{hour} with GHI={ghi}, DNI={dni}, DHI={dhi}, "
379
  f"dry_bulb={outdoor_temp} for {component_name} ({component.component_type.value})")
380
 
381
+ year = 2025
 
382
  n = TFMCalculations.day_of_year(month, day, year)
383
  EOT = TFMCalculations.equation_of_time(n)
384
+ lambda_std = 15 * timezone
385
+ standard_time = hour - 1 + 0.5
386
  LST = standard_time + (4 * (lambda_std - longitude) + EOT) / 60
387
 
 
388
  delta = 23.45 * math.sin(math.radians(360 / 365 * (284 + n)))
 
 
 
 
 
389
  phi = math.radians(latitude)
390
  delta_rad = math.radians(delta)
391
+ hra = 15 * (LST - 12)
392
  hra_rad = math.radians(hra)
393
 
394
  sin_alpha = math.sin(phi) * math.sin(delta_rad) + math.cos(phi) * math.cos(delta_rad) * math.cos(hra_rad)
395
  alpha = math.degrees(math.asin(sin_alpha))
396
 
397
  if abs(math.cos(math.radians(alpha))) < 0.01:
398
+ azimuth = 0
399
  else:
400
  sin_az = math.cos(delta_rad) * math.sin(hra_rad) / math.cos(math.radians(alpha))
401
  cos_az = (sin_alpha * math.sin(phi) - math.sin(delta_rad)) / (math.cos(math.radians(alpha)) * math.cos(phi))
402
  azimuth = math.degrees(math.atan2(sin_az, cos_az))
403
+ if hra > 0:
404
  azimuth = 360 - azimuth if azimuth > 0 else -azimuth
405
 
406
  logger.info(f"Solar angles for {month}/{day}/{hour}: declination={delta:.2f}, LST={LST:.2f}, "
407
  f"HRA={hra:.2f}, altitude={alpha:.2f}, azimuth={azimuth:.2f} for {component_name} ({component.component_type.value})")
408
 
 
409
  building_info = {"orientation_angle": building_orientation}
410
  try:
411
+ surface_tilt, surface_azimuth, h_o, emissivity, absorptivity = \
412
  TFMCalculations.get_surface_parameters(
413
  component, building_info, material_library, project_materials,
414
+ project_constructions, project_glazing_materials
415
  )
416
  except Exception as e:
417
  logger.error(f"Error getting surface parameters for {component_name}: {str(e)}. Using defaults.")
 
418
  if component.component_type == ComponentType.ROOF:
419
+ surface_tilt = 0.0
420
+ surface_azimuth = 0.0
421
  elif component.component_type == ComponentType.SKYLIGHT:
422
+ surface_tilt = 0.0
423
+ surface_azimuth = 0.0
424
  elif component.component_type == ComponentType.FLOOR:
425
+ surface_tilt = 180.0
426
+ surface_azimuth = 0.0
427
+ else: # WALL, WINDOW
428
+ surface_tilt = 90.0
429
+ surface_azimuth = 0.0
430
 
431
+ if component.component_type in [ComponentType.WALL, ComponentType.ROOF]:
432
+ absorptivity = 0.6
 
433
  h_o = 17.0 if component.component_type == ComponentType.WALL else 23.0
434
  else: # WINDOW, SKYLIGHT
435
+ absorptivity = 0.0
436
  h_o = 17.0 if component.component_type == ComponentType.WINDOW else 23.0
437
 
 
 
438
  alpha_rad = math.radians(alpha)
439
  surface_tilt_rad = math.radians(surface_tilt)
440
  azimuth_rad = math.radians(azimuth)
441
  surface_azimuth_rad = math.radians(surface_azimuth)
442
 
 
443
  cos_theta = (math.sin(alpha_rad) * math.cos(surface_tilt_rad) +
444
  math.cos(alpha_rad) * math.sin(surface_tilt_rad) *
445
  math.cos(azimuth_rad - surface_azimuth_rad))
446
 
 
447
  cos_theta = max(min(cos_theta, 1.0), 0.0)
448
 
 
449
  logger.info(f" Component {component_name} ({component.component_type.value}) at {month}/{day}/{hour}: "
450
  f"surface_tilt={surface_tilt:.2f}, surface_azimuth={surface_azimuth:.2f}, "
451
  f"cos_theta={cos_theta:.4f}")
452
 
 
 
453
  view_factor = (1 - math.cos(surface_tilt_rad)) / 2
 
 
454
  ground_reflected = ground_reflectivity * ghi * view_factor
455
 
456
+ if cos_theta > 0:
 
457
  I_t = dni * cos_theta + dhi + ground_reflected
458
+ else:
459
  I_t = dhi + ground_reflected
460
 
 
461
  solar_heat_gain = 0.0
462
 
463
  if component.component_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]:
464
+ shgc = 0.7
 
465
  glazing_material = getattr(component, 'glazing_material', None)
466
  if glazing_material:
467
  glazing_material_obj = None
 
474
  h_o = getattr(glazing_material_obj, 'h_o', h_o)
475
  else:
476
  logger.warning(f"Glazing material not found for {component_name} ({component.component_type.value}). Using default SHGC=0.7.")
 
 
477
 
478
+ glazing_type = "Single Clear"
 
479
  if hasattr(component, 'name') and component.name in TFMCalculations.GLAZING_TYPE_MAPPING:
480
  glazing_type = TFMCalculations.GLAZING_TYPE_MAPPING[component.name]
481
 
482
+ iac = getattr(component, 'iac', 1.0)
 
483
 
 
484
  shgc_dynamic = shgc * TFMCalculations.calculate_dynamic_shgc(glazing_type, cos_theta)
485
 
486
+ solar_heat_gain = component.area * shgc_dynamic * I_t * iac / 1000
 
487
 
488
  logger.info(f"Fenestration solar heat gain for {component_name} ({component.component_type.value}) at {month}/{day}/{hour}: "
489
  f"{solar_heat_gain:.4f} kW (area={component.area}, shgc_dynamic={shgc_dynamic:.4f}, "
490
  f"I_t={I_t:.2f}, iac={iac})")
491
 
492
+ elif component.component_type in [ComponentType.WALL, ComponentType.ROOF]:
493
+ surface_resistance = 1/h_o
 
494
 
495
+ solar_heat_gain = component.area * absorptivity * I_t * surface_resistance / 1000
 
496
 
497
  logger.info(f"Opaque surface solar heat gain for {component_name} ({component.component_type.value}) at {month}/{day}/{hour}: "
498
+ f"{solar_heat_gain:.4f} kW (area={component.area}, absorptivity={absorptivity:.2f}, "
499
  f"I_t={I_t:.2f}, surface_resistance={surface_resistance:.4f})")
500
 
501
  return solar_heat_gain
 
520
  fraction = min(lighting_operating_hours, operation_hours) / operation_hours if operation_hours > 0 else 0
521
  lighting_load = lpd * area * fraction
522
  total_load += lighting_load
523
+ equipment = internal_loads.get("equipment", {})
524
+ total_power_density = equipment.get("total_power_density", 0)
525
+ equipment_load = total_power_density * area
526
+ total_load += equipment_load
 
527
  return total_load / 1000
528
 
529
  @staticmethod
 
531
  """Calculate ventilation load for heating and cooling in kW based on mode."""
532
  if mode == "none":
533
  return 0, 0
534
+ ventilation = internal_loads.get("ventilation", {})
535
  if not ventilation:
536
  return 0, 0
537
+ space_rate = ventilation.get("space_rate", 0.3)
538
+ people_rate = ventilation.get("people_rate", 2.5)
539
  num_people = sum(group["num_people"] for group in internal_loads.get("people", []))
540
+ ventilation_flow = (space_rate * area + people_rate * num_people) / 1000
541
+ air_density = 1.2
542
+ specific_heat = 1000
543
  delta_t = outdoor_temp - indoor_temp
544
  if mode == "cooling" and delta_t <= 0:
545
  return 0, 0
546
  if mode == "heating" and delta_t >= 0:
547
  return 0, 0
548
+ load = ventilation_flow * air_density * specific_heat * delta_t / 1000
549
  cooling_load = load if mode == "cooling" else 0
550
  heating_load = -load if mode == "heating" else 0
551
  return cooling_load, heating_load
 
555
  """Calculate infiltration load for heating and cooling in kW based on mode."""
556
  if mode == "none":
557
  return 0, 0
558
+ infiltration = internal_loads.get("infiltration", {})
559
  if not infiltration:
560
  return 0, 0
561
  method = infiltration.get("method", "ACH")
562
  settings = infiltration.get("settings", {})
563
  building_height = building_info.get("building_height", 3.0)
564
+ volume = area * building_height
565
+ air_density = 1.2
566
+ specific_heat = 1000
567
  delta_t = outdoor_temp - indoor_temp
568
  if mode == "cooling" and delta_t <= 0:
569
  return 0, 0
 
571
  return 0, 0
572
  if method == "ACH":
573
  ach = settings.get("rate", 0.5)
574
+ infiltration_flow = ach * volume / 3600
575
  elif method == "Crack Flow":
576
+ ela = settings.get("ela", 0.0001)
577
+ wind_speed = 4.0
578
+ infiltration_flow = ela * area * wind_speed / 2
579
  else: # Empirical Equations
580
  c = settings.get("c", 0.1)
581
  n = settings.get("n", 0.65)
582
  delta_t_abs = abs(delta_t)
583
+ infiltration_flow = c * (delta_t_abs ** n) * area / 3600
584
+ load = infiltration_flow * air_density * specific_heat * delta_t / 1000
585
  cooling_load = load if mode == "cooling" else 0
586
  heating_load = -load if mode == "heating" else 0
587
  return cooling_load, heating_load
 
591
  """Calculate adaptive comfort temperature per ASHRAE 55."""
592
  if 10 <= outdoor_temp <= 33.5:
593
  return 0.31 * outdoor_temp + 17.8
594
+ return 24.0
595
 
596
  @staticmethod
597
  def filter_hourly_data(hourly_data: List[Dict], sim_period: Dict, climate_data: Dict) -> List[Dict]:
 
629
  }
630
  elif mode == "heating":
631
  return {
632
+ "temperature": indoor_conditions.get("heating_setpoint", {}).get("temperature", 20.0),
633
  "rh": indoor_conditions.get("heating_setpoint", {}).get("rh", 50.0)
634
  }
635
  else:
 
654
  operating_periods = hvac_settings.get("operating_hours", [{"start": 8, "end": 18}])
655
  area = building_info.get("floor_area", 100.0)
656
 
 
657
  if "material_library" not in st.session_state:
658
+ from app.material_library import MaterialLibrary
659
  st.session_state.material_library = MaterialLibrary()
660
  logger.info("Initialized MaterialLibrary in session_state for solar calculations")
661
 
 
662
  for comp_list in components.values():
663
  for comp in comp_list:
664
  comp.ctf = CTFCalculator.calculate_ctf_coefficients(comp)
 
668
  outdoor_temp = hour_data["dry_bulb"]
669
  indoor_cond = TFMCalculations.get_indoor_conditions(indoor_conditions, hour, outdoor_temp)
670
  indoor_temp = indoor_cond["temperature"]
 
671
  conduction_cooling = conduction_heating = solar = internal = ventilation_cooling = ventilation_heating = infiltration_cooling = infiltration_heating = 0
 
672
  is_operating = False
673
  for period in operating_periods:
674
  start_hour = period.get("start", 8)
 
676
  if start_hour <= hour % 24 <= end_hour:
677
  is_operating = True
678
  break
 
679
  mode = "none" if abs(outdoor_temp - 18) < 0.01 else "cooling" if outdoor_temp > 18 else "heating"
680
  if is_operating and mode == "cooling":
 
681
  for comp_list in components.values():
682
  for comp in comp_list:
683
  cool_load, _ = TFMCalculations.calculate_conduction_load(comp, outdoor_temp, indoor_temp, hour, mode="cooling")
684
  conduction_cooling += cool_load
 
 
685
  component_solar_load = TFMCalculations.calculate_solar_load(comp, hour_data, hour, building_orientation, mode="cooling")
686
  solar += component_solar_load
 
 
687
  logger.info(f"Component {comp.name} ({comp.component_type.value}) solar load: {component_solar_load:.3f} kW, accumulated solar: {solar:.3f} kW")
688
 
689
  internal = TFMCalculations.calculate_internal_load(internal_loads, hour, max([p["end"] - p["start"] for p in operating_periods]), area)
 
697
  internal = TFMCalculations.calculate_internal_load(internal_loads, hour, max([p["end"] - p["start"] for p in operating_periods]), area)
698
  _, ventilation_heating = TFMCalculations.calculate_ventilation_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, mode="heating")
699
  _, infiltration_heating = TFMCalculations.calculate_infiltration_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, mode="heating")
700
+ else:
701
+ internal = 0
702
 
 
703
  logger.info(f"Hour {hour} total loads - conduction: {conduction_cooling:.3f} kW, solar: {solar:.3f} kW, internal: {internal:.3f} kW")
704
 
 
705
  total_cooling = conduction_cooling + solar + internal + ventilation_cooling + infiltration_cooling
706
  total_heating = max(conduction_heating + ventilation_heating + infiltration_heating - internal, 0)
 
707
  if mode == "cooling":
708
  total_heating = 0
709
  elif mode == "heating":
 
723
  "total_cooling": total_cooling,
724
  "total_heating": total_heating
725
  })
 
726
  loads_by_day = defaultdict(list)
727
  for load in temp_loads:
728
  day_key = (load["month"], load["day"])
729
  loads_by_day[day_key].append(load)
730
  final_loads = []
731
  for day_key, day_loads in loads_by_day.items():
 
732
  cooling_hours = sum(1 for load in day_loads if load["total_cooling"] > 0)
733
  heating_hours = sum(1 for load in day_loads if load["total_heating"] > 0)
 
734
  for load in day_loads:
735
  if cooling_hours > heating_hours:
736
+ load["total_heating"] = 0
737
  elif heating_hours > cooling_hours:
 
 
738
  load["total_cooling"] = 0
739
+ else:
740
+ load["total_cooling"] = 0
741
+ load["total_heating"] = 0
742
  final_loads.append(load)
743
  return final_loads