Spaces:
Sleeping
Sleeping
Update utils/solar.py
Browse files- utils/solar.py +44 -38
utils/solar.py
CHANGED
@@ -288,10 +288,10 @@ class SolarCalculations:
|
|
288 |
) -> List[Dict[str, Any]]:
|
289 |
"""
|
290 |
Calculate solar angles, sol-air temperature, and solar heat gain for hourly data with global_horizontal_radiation > 0.
|
291 |
-
|
292 |
Uses the Perez model for diffuse radiation on tilted surfaces, accounting for anisotropic sky conditions
|
293 |
(circumsolar and horizon brightening). Direct and ground-reflected radiation follow ASHRAE isotropic models.
|
294 |
-
|
295 |
Args:
|
296 |
hourly_data (List[Dict]): Hourly weather data containing month, day, hour, global_horizontal_radiation,
|
297 |
direct_normal_radiation, diffuse_horizontal_radiation, dry_bulb, dew_point,
|
@@ -302,21 +302,21 @@ class SolarCalculations:
|
|
302 |
ground_reflectivity (float): Ground reflectivity (albedo, typically 0.2).
|
303 |
components (Dict[str, List]): Dictionary of component lists (e.g., walls, windows) with id, area,
|
304 |
type, facade, construction, fenestration, or door_material.
|
305 |
-
|
306 |
Returns:
|
307 |
List[Dict]: List of results for each hour with global_horizontal_radiation > 0, containing solar angles
|
308 |
and per-component results (total_incident_radiation, sol_air_temp, solar_heat_gain, etc.).
|
309 |
-
|
310 |
Raises:
|
311 |
ValueError: If required weather data or component parameters are missing or invalid.
|
312 |
-
|
313 |
References:
|
314 |
ASHRAE Handbook—Fundamentals (2021), Chapters 14 and 15.
|
315 |
Duffie & Beckman, Solar Engineering of Thermal Processes, 4th Ed., Section 2.16.
|
316 |
"""
|
317 |
year = 2025 # Fixed year since not provided in data
|
318 |
results = []
|
319 |
-
|
320 |
# Validate input parameters
|
321 |
if not -90 <= latitude <= 90:
|
322 |
logger.warning(f"Invalid latitude {latitude}. Using default 0.0.")
|
@@ -330,12 +330,12 @@ class SolarCalculations:
|
|
330 |
if not 0 <= ground_reflectivity <= 1:
|
331 |
logger.warning(f"Invalid ground_reflectivity {ground_reflectivity}. Using default 0.2.")
|
332 |
ground_reflectivity = 0.2
|
333 |
-
|
334 |
logger.info(f"Using parameters: latitude={latitude}, longitude={longitude}, timezone={timezone}, "
|
335 |
f"ground_reflectivity={ground_reflectivity}")
|
336 |
-
|
337 |
lambda_std = 15 * timezone # Standard meridian longitude (°)
|
338 |
-
|
339 |
# Cache facade azimuths (used only for walls, windows)
|
340 |
building_info = components.get("_building_info", {})
|
341 |
facade_cache = {
|
@@ -344,7 +344,7 @@ class SolarCalculations:
|
|
344 |
"C": (building_info.get("orientation_angle", 0.0) + 180.0) % 360,
|
345 |
"D": (building_info.get("orientation_angle", 0.0) + 270.0) % 360
|
346 |
}
|
347 |
-
|
348 |
for record in hourly_data:
|
349 |
# Step 1: Extract and validate data
|
350 |
month = record.get("month")
|
@@ -363,44 +363,44 @@ class SolarCalculations:
|
|
363 |
logger.warning(f"Diffuse radiation {diffuse_horizontal_radiation} exceeds global {global_horizontal_radiation} "
|
364 |
f"at {month}/{day}/{hour}. Capping diffuse to global.")
|
365 |
diffuse_horizontal_radiation = global_horizontal_radiation
|
366 |
-
|
367 |
if None in [month, day, hour, outdoor_temp]:
|
368 |
logger.error(f"Missing required weather data for {month}/{day}/{hour}")
|
369 |
raise ValueError(f"Missing required weather data for {month}/{day}/{hour}")
|
370 |
-
|
371 |
if global_horizontal_radiation < 0 or direct_normal_radiation < 0 or diffuse_horizontal_radiation < 0:
|
372 |
logger.error(f"Negative radiation values for {month}/{day}/{hour}")
|
373 |
raise ValueError(f"Negative radiation values for {month}/{day}/{hour}")
|
374 |
-
|
375 |
if global_horizontal_radiation <= 0:
|
376 |
logger.info(f"Skipping hour {month}/{day}/{hour} due to global_horizontal_radiation={global_horizontal_radiation} <= 0")
|
377 |
continue # Skip hours with no solar radiation
|
378 |
-
|
379 |
logger.info(f"Processing solar for {month}/{day}/{hour} with global_horizontal_radiation={global_horizontal_radiation}, "
|
380 |
f"direct_normal_radiation={direct_normal_radiation}, diffuse_horizontal_radiation={diffuse_horizontal_radiation}, "
|
381 |
f"dry_bulb={outdoor_temp}, dew_point={dew_point}, wind_speed={wind_speed}, "
|
382 |
f"total_sky_cover={total_sky_cover}")
|
383 |
-
|
384 |
# Step 2: Local Solar Time (LST) with Equation of Time
|
385 |
n = self.day_of_year(month, day, year)
|
386 |
EOT = self.equation_of_time(n)
|
387 |
standard_time = hour - 1 + 0.5 # Convert to decimal, assume mid-hour
|
388 |
LST = standard_time + (4 * (lambda_std - longitude) + EOT) / 60
|
389 |
-
|
390 |
# Step 3: Solar Declination (δ)
|
391 |
delta = 23.45 * math.sin(math.radians(360 / 365 * (284 + n)))
|
392 |
-
|
393 |
# Step 4: Hour Angle (HRA)
|
394 |
hra = 15 * (LST - 12)
|
395 |
-
|
396 |
# Step 5: Solar Altitude (α) and Azimuth (ψ)
|
397 |
phi = math.radians(latitude)
|
398 |
delta_rad = math.radians(delta)
|
399 |
hra_rad = math.radians(hra)
|
400 |
-
|
401 |
sin_alpha = math.sin(phi) * math.sin(delta_rad) + math.cos(phi) * math.cos(delta_rad) * math.cos(hra_rad)
|
402 |
alpha = math.degrees(math.asin(sin_alpha))
|
403 |
-
|
404 |
if abs(math.cos(math.radians(alpha))) < 0.01:
|
405 |
azimuth = 0 # North at sunrise/sunset
|
406 |
else:
|
@@ -409,10 +409,10 @@ class SolarCalculations:
|
|
409 |
azimuth = math.degrees(math.atan2(sin_az, cos_az))
|
410 |
if hra > 0: # Afternoon
|
411 |
azimuth = 360 - azimuth if azimuth > 0 else -azimuth
|
412 |
-
|
413 |
logger.info(f"Solar angles for {month}/{day}/{hour}: declination={delta:.2f}, LST={LST:.2f}, "
|
414 |
f"HRA={hra:.2f}, altitude={alpha:.2f}, azimuth={azimuth:.2f}")
|
415 |
-
|
416 |
# Calculate clearness index (kt) and Perez coefficients once per hour
|
417 |
zenith_deg = 90 - alpha # Zenith angle = 90 - altitude
|
418 |
if global_horizontal_radiation > 0 and zenith_deg < 90:
|
@@ -421,7 +421,7 @@ class SolarCalculations:
|
|
421 |
f1, f2 = self.calculate_perez_coefficients(kt, zenith_deg)
|
422 |
else:
|
423 |
kt, f1, f2 = 0.0, 0.0, 0.0
|
424 |
-
|
425 |
# Step 6: Component-specific calculations
|
426 |
component_results = []
|
427 |
for comp_type, comp_list in components.items():
|
@@ -433,7 +433,7 @@ class SolarCalculations:
|
|
433 |
if comp.get('adiabatic', False) and comp.get('ground_contact', False):
|
434 |
logger.warning(f"Component {comp.get('name', 'unknown_component')} has both adiabatic and ground_contact set to True. Treating as adiabatic, setting ground_contact to False.")
|
435 |
comp['ground_contact'] = False
|
436 |
-
|
437 |
# Get surface parameters
|
438 |
surface_tilt, surface_azimuth, h_o, emissivity, absorptivity = \
|
439 |
self.get_surface_parameters(comp, building_info)
|
@@ -441,17 +441,17 @@ class SolarCalculations:
|
|
441 |
# For windows/skylights, get SHGC from component
|
442 |
shgc = comp.get('shgc', 0.7)
|
443 |
fenestration_name = comp.get('fenestration', None)
|
444 |
-
|
445 |
# Calculate angle of incidence (θ)
|
446 |
cos_theta = (math.sin(math.radians(alpha)) * math.cos(math.radians(surface_tilt)) +
|
447 |
math.cos(math.radians(alpha)) * math.sin(math.radians(surface_tilt)) *
|
448 |
math.cos(math.radians(azimuth - surface_azimuth)))
|
449 |
cos_theta = max(min(cos_theta, 1.0), 0.0) # Clamp to [0, 1]
|
450 |
-
|
451 |
logger.info(f" Component {comp.get('name', 'unknown_component')} at {month}/{day}/{hour}: "
|
452 |
f"surface_tilt={surface_tilt:.2f}, surface_azimuth={surface_azimuth:.2f}, "
|
453 |
-
f"cos_theta={cos_theta:.2f}
|
454 |
-
|
455 |
# Calculate total incident radiation (I_t) with Perez model
|
456 |
view_factor = (1 - math.cos(math.radians(surface_tilt))) / 2
|
457 |
ground_reflected = ground_reflectivity * global_horizontal_radiation * view_factor
|
@@ -466,7 +466,7 @@ class SolarCalculations:
|
|
466 |
direct_tilted = direct_normal_radiation * max(cos_theta, 0.0)
|
467 |
I_t = direct_tilted + diffuse_tilted + ground_reflected
|
468 |
I_t = max(I_t, 0.0) # Ensure non-negative radiation
|
469 |
-
|
470 |
# Initialize result
|
471 |
comp_result = {
|
472 |
"component_id": comp.get('name', 'unknown_component'),
|
@@ -474,13 +474,13 @@ class SolarCalculations:
|
|
474 |
"absorptivity": round(absorptivity, 2),
|
475 |
"emissivity": round(emissivity, 2) if emissivity is not None else None
|
476 |
}
|
477 |
-
|
478 |
# Skip calculations for adiabatic surfaces
|
479 |
if comp.get('adiabatic', False):
|
480 |
logger.info(f"Skipping solar calculations for adiabatic component {comp_result['component_id']} at {month}/{day}/{hour}")
|
481 |
component_results.append(comp_result)
|
482 |
continue
|
483 |
-
|
484 |
# Handle ground-contact surfaces
|
485 |
if comp.get('ground_contact', False):
|
486 |
# Validate component type
|
@@ -490,7 +490,7 @@ class SolarCalculations:
|
|
490 |
logger.warning(f"Invalid ground-contact component type '{component_type}' for {comp_result['component_id']}. Skipping ground temperature assignment.")
|
491 |
component_results.append(comp_result)
|
492 |
continue
|
493 |
-
|
494 |
# Retrieve ground temperature
|
495 |
climate_data = st.session_state.project_data.get("climate_data", {})
|
496 |
ground_temperatures = climate_data.get("ground_temperatures", {})
|
@@ -504,7 +504,7 @@ class SolarCalculations:
|
|
504 |
logger.info(f"Ground-contact component {comp_result['component_id']} at {month}/{day}/{hour}: ground_temp={ground_temp:.2f}°C")
|
505 |
component_results.append(comp_result)
|
506 |
continue
|
507 |
-
|
508 |
# Calculate sol-air temperature for opaque surfaces (non-ground-contact)
|
509 |
if comp.get('type', '').lower() in ['walls', 'roofs'] and not comp.get('ground_contact', False):
|
510 |
T_sol_air = CTFCalculator.calculate_sol_air_temperature(
|
@@ -513,26 +513,32 @@ class SolarCalculations:
|
|
513 |
)
|
514 |
comp_result["sol_air_temp"] = round(T_sol_air, 2)
|
515 |
logger.info(f"Sol-air temp for {comp_result['component_id']} at {month}/{day}/{hour}: {T_sol_air:.2f}°C")
|
516 |
-
|
517 |
# Calculate solar heat gain for fenestration
|
518 |
elif comp.get('type', '').lower() in ['windows', 'skylights']:
|
519 |
glazing_type = self.GLAZING_TYPE_MAPPING.get(comp.get('fenestration', ''), 'Single Clear')
|
520 |
iac = comp.get('shading_coefficient', 1.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
521 |
shgc_dynamic = shgc * self.calculate_dynamic_shgc(glazing_type, cos_theta)
|
522 |
solar_heat_gain = comp.get('area', 0.0) * shgc_dynamic * I_t * iac / 1000 # kW
|
523 |
comp_result["solar_heat_gain"] = round(solar_heat_gain, 2)
|
524 |
comp_result["shgc_dynamic"] = round(shgc_dynamic, 2)
|
525 |
logger.info(f"Solar heat gain for {comp_result['component_id']} at {month}/{day}/{hour}: "
|
526 |
f"{solar_heat_gain:.2f} kW (area={comp.get('area', 0.0)}, shgc_dynamic={shgc_dynamic:.2f}, "
|
527 |
-
f"I_t={I_t:.2f}, iac={iac})")
|
528 |
|
529 |
component_results.append(comp_result)
|
530 |
-
|
531 |
except Exception as e:
|
532 |
component_name = comp.get('name', 'unknown_component')
|
533 |
logger.error(f"Error processing component {component_name} at {month}/{day}/{hour}: {str(e)}")
|
534 |
continue
|
535 |
-
|
536 |
# Store results for this hour
|
537 |
result = {
|
538 |
"month": month,
|
@@ -546,5 +552,5 @@ class SolarCalculations:
|
|
546 |
"component_results": component_results
|
547 |
}
|
548 |
results.append(result)
|
549 |
-
|
550 |
return results
|
|
|
288 |
) -> List[Dict[str, Any]]:
|
289 |
"""
|
290 |
Calculate solar angles, sol-air temperature, and solar heat gain for hourly data with global_horizontal_radiation > 0.
|
291 |
+
|
292 |
Uses the Perez model for diffuse radiation on tilted surfaces, accounting for anisotropic sky conditions
|
293 |
(circumsolar and horizon brightening). Direct and ground-reflected radiation follow ASHRAE isotropic models.
|
294 |
+
|
295 |
Args:
|
296 |
hourly_data (List[Dict]): Hourly weather data containing month, day, hour, global_horizontal_radiation,
|
297 |
direct_normal_radiation, diffuse_horizontal_radiation, dry_bulb, dew_point,
|
|
|
302 |
ground_reflectivity (float): Ground reflectivity (albedo, typically 0.2).
|
303 |
components (Dict[str, List]): Dictionary of component lists (e.g., walls, windows) with id, area,
|
304 |
type, facade, construction, fenestration, or door_material.
|
305 |
+
|
306 |
Returns:
|
307 |
List[Dict]: List of results for each hour with global_horizontal_radiation > 0, containing solar angles
|
308 |
and per-component results (total_incident_radiation, sol_air_temp, solar_heat_gain, etc.).
|
309 |
+
|
310 |
Raises:
|
311 |
ValueError: If required weather data or component parameters are missing or invalid.
|
312 |
+
|
313 |
References:
|
314 |
ASHRAE Handbook—Fundamentals (2021), Chapters 14 and 15.
|
315 |
Duffie & Beckman, Solar Engineering of Thermal Processes, 4th Ed., Section 2.16.
|
316 |
"""
|
317 |
year = 2025 # Fixed year since not provided in data
|
318 |
results = []
|
319 |
+
|
320 |
# Validate input parameters
|
321 |
if not -90 <= latitude <= 90:
|
322 |
logger.warning(f"Invalid latitude {latitude}. Using default 0.0.")
|
|
|
330 |
if not 0 <= ground_reflectivity <= 1:
|
331 |
logger.warning(f"Invalid ground_reflectivity {ground_reflectivity}. Using default 0.2.")
|
332 |
ground_reflectivity = 0.2
|
333 |
+
|
334 |
logger.info(f"Using parameters: latitude={latitude}, longitude={longitude}, timezone={timezone}, "
|
335 |
f"ground_reflectivity={ground_reflectivity}")
|
336 |
+
|
337 |
lambda_std = 15 * timezone # Standard meridian longitude (°)
|
338 |
+
|
339 |
# Cache facade azimuths (used only for walls, windows)
|
340 |
building_info = components.get("_building_info", {})
|
341 |
facade_cache = {
|
|
|
344 |
"C": (building_info.get("orientation_angle", 0.0) + 180.0) % 360,
|
345 |
"D": (building_info.get("orientation_angle", 0.0) + 270.0) % 360
|
346 |
}
|
347 |
+
|
348 |
for record in hourly_data:
|
349 |
# Step 1: Extract and validate data
|
350 |
month = record.get("month")
|
|
|
363 |
logger.warning(f"Diffuse radiation {diffuse_horizontal_radiation} exceeds global {global_horizontal_radiation} "
|
364 |
f"at {month}/{day}/{hour}. Capping diffuse to global.")
|
365 |
diffuse_horizontal_radiation = global_horizontal_radiation
|
366 |
+
|
367 |
if None in [month, day, hour, outdoor_temp]:
|
368 |
logger.error(f"Missing required weather data for {month}/{day}/{hour}")
|
369 |
raise ValueError(f"Missing required weather data for {month}/{day}/{hour}")
|
370 |
+
|
371 |
if global_horizontal_radiation < 0 or direct_normal_radiation < 0 or diffuse_horizontal_radiation < 0:
|
372 |
logger.error(f"Negative radiation values for {month}/{day}/{hour}")
|
373 |
raise ValueError(f"Negative radiation values for {month}/{day}/{hour}")
|
374 |
+
|
375 |
if global_horizontal_radiation <= 0:
|
376 |
logger.info(f"Skipping hour {month}/{day}/{hour} due to global_horizontal_radiation={global_horizontal_radiation} <= 0")
|
377 |
continue # Skip hours with no solar radiation
|
378 |
+
|
379 |
logger.info(f"Processing solar for {month}/{day}/{hour} with global_horizontal_radiation={global_horizontal_radiation}, "
|
380 |
f"direct_normal_radiation={direct_normal_radiation}, diffuse_horizontal_radiation={diffuse_horizontal_radiation}, "
|
381 |
f"dry_bulb={outdoor_temp}, dew_point={dew_point}, wind_speed={wind_speed}, "
|
382 |
f"total_sky_cover={total_sky_cover}")
|
383 |
+
|
384 |
# Step 2: Local Solar Time (LST) with Equation of Time
|
385 |
n = self.day_of_year(month, day, year)
|
386 |
EOT = self.equation_of_time(n)
|
387 |
standard_time = hour - 1 + 0.5 # Convert to decimal, assume mid-hour
|
388 |
LST = standard_time + (4 * (lambda_std - longitude) + EOT) / 60
|
389 |
+
|
390 |
# Step 3: Solar Declination (δ)
|
391 |
delta = 23.45 * math.sin(math.radians(360 / 365 * (284 + n)))
|
392 |
+
|
393 |
# Step 4: Hour Angle (HRA)
|
394 |
hra = 15 * (LST - 12)
|
395 |
+
|
396 |
# Step 5: Solar Altitude (α) and Azimuth (ψ)
|
397 |
phi = math.radians(latitude)
|
398 |
delta_rad = math.radians(delta)
|
399 |
hra_rad = math.radians(hra)
|
400 |
+
|
401 |
sin_alpha = math.sin(phi) * math.sin(delta_rad) + math.cos(phi) * math.cos(delta_rad) * math.cos(hra_rad)
|
402 |
alpha = math.degrees(math.asin(sin_alpha))
|
403 |
+
|
404 |
if abs(math.cos(math.radians(alpha))) < 0.01:
|
405 |
azimuth = 0 # North at sunrise/sunset
|
406 |
else:
|
|
|
409 |
azimuth = math.degrees(math.atan2(sin_az, cos_az))
|
410 |
if hra > 0: # Afternoon
|
411 |
azimuth = 360 - azimuth if azimuth > 0 else -azimuth
|
412 |
+
|
413 |
logger.info(f"Solar angles for {month}/{day}/{hour}: declination={delta:.2f}, LST={LST:.2f}, "
|
414 |
f"HRA={hra:.2f}, altitude={alpha:.2f}, azimuth={azimuth:.2f}")
|
415 |
+
|
416 |
# Calculate clearness index (kt) and Perez coefficients once per hour
|
417 |
zenith_deg = 90 - alpha # Zenith angle = 90 - altitude
|
418 |
if global_horizontal_radiation > 0 and zenith_deg < 90:
|
|
|
421 |
f1, f2 = self.calculate_perez_coefficients(kt, zenith_deg)
|
422 |
else:
|
423 |
kt, f1, f2 = 0.0, 0.0, 0.0
|
424 |
+
|
425 |
# Step 6: Component-specific calculations
|
426 |
component_results = []
|
427 |
for comp_type, comp_list in components.items():
|
|
|
433 |
if comp.get('adiabatic', False) and comp.get('ground_contact', False):
|
434 |
logger.warning(f"Component {comp.get('name', 'unknown_component')} has both adiabatic and ground_contact set to True. Treating as adiabatic, setting ground_contact to False.")
|
435 |
comp['ground_contact'] = False
|
436 |
+
|
437 |
# Get surface parameters
|
438 |
surface_tilt, surface_azimuth, h_o, emissivity, absorptivity = \
|
439 |
self.get_surface_parameters(comp, building_info)
|
|
|
441 |
# For windows/skylights, get SHGC from component
|
442 |
shgc = comp.get('shgc', 0.7)
|
443 |
fenestration_name = comp.get('fenestration', None)
|
444 |
+
|
445 |
# Calculate angle of incidence (θ)
|
446 |
cos_theta = (math.sin(math.radians(alpha)) * math.cos(math.radians(surface_tilt)) +
|
447 |
math.cos(math.radians(alpha)) * math.sin(math.radians(surface_tilt)) *
|
448 |
math.cos(math.radians(azimuth - surface_azimuth)))
|
449 |
cos_theta = max(min(cos_theta, 1.0), 0.0) # Clamp to [0, 1]
|
450 |
+
|
451 |
logger.info(f" Component {comp.get('name', 'unknown_component')} at {month}/{day}/{hour}: "
|
452 |
f"surface_tilt={surface_tilt:.2f}, surface_azimuth={surface_azimuth:.2f}, "
|
453 |
+
f"cos_theta={cos_theta:.2f}")
|
454 |
+
|
455 |
# Calculate total incident radiation (I_t) with Perez model
|
456 |
view_factor = (1 - math.cos(math.radians(surface_tilt))) / 2
|
457 |
ground_reflected = ground_reflectivity * global_horizontal_radiation * view_factor
|
|
|
466 |
direct_tilted = direct_normal_radiation * max(cos_theta, 0.0)
|
467 |
I_t = direct_tilted + diffuse_tilted + ground_reflected
|
468 |
I_t = max(I_t, 0.0) # Ensure non-negative radiation
|
469 |
+
|
470 |
# Initialize result
|
471 |
comp_result = {
|
472 |
"component_id": comp.get('name', 'unknown_component'),
|
|
|
474 |
"absorptivity": round(absorptivity, 2),
|
475 |
"emissivity": round(emissivity, 2) if emissivity is not None else None
|
476 |
}
|
477 |
+
|
478 |
# Skip calculations for adiabatic surfaces
|
479 |
if comp.get('adiabatic', False):
|
480 |
logger.info(f"Skipping solar calculations for adiabatic component {comp_result['component_id']} at {month}/{day}/{hour}")
|
481 |
component_results.append(comp_result)
|
482 |
continue
|
483 |
+
|
484 |
# Handle ground-contact surfaces
|
485 |
if comp.get('ground_contact', False):
|
486 |
# Validate component type
|
|
|
490 |
logger.warning(f"Invalid ground-contact component type '{component_type}' for {comp_result['component_id']}. Skipping ground temperature assignment.")
|
491 |
component_results.append(comp_result)
|
492 |
continue
|
493 |
+
|
494 |
# Retrieve ground temperature
|
495 |
climate_data = st.session_state.project_data.get("climate_data", {})
|
496 |
ground_temperatures = climate_data.get("ground_temperatures", {})
|
|
|
504 |
logger.info(f"Ground-contact component {comp_result['component_id']} at {month}/{day}/{hour}: ground_temp={ground_temp:.2f}°C")
|
505 |
component_results.append(comp_result)
|
506 |
continue
|
507 |
+
|
508 |
# Calculate sol-air temperature for opaque surfaces (non-ground-contact)
|
509 |
if comp.get('type', '').lower() in ['walls', 'roofs'] and not comp.get('ground_contact', False):
|
510 |
T_sol_air = CTFCalculator.calculate_sol_air_temperature(
|
|
|
513 |
)
|
514 |
comp_result["sol_air_temp"] = round(T_sol_air, 2)
|
515 |
logger.info(f"Sol-air temp for {comp_result['component_id']} at {month}/{day}/{hour}: {T_sol_air:.2f}°C")
|
516 |
+
|
517 |
# Calculate solar heat gain for fenestration
|
518 |
elif comp.get('type', '').lower() in ['windows', 'skylights']:
|
519 |
glazing_type = self.GLAZING_TYPE_MAPPING.get(comp.get('fenestration', ''), 'Single Clear')
|
520 |
iac = comp.get('shading_coefficient', 1.0)
|
521 |
+
# Adjust shading coefficient based on shading type
|
522 |
+
shading_type = comp.get('shading_type', 'No shading')
|
523 |
+
if shading_type == "External Shading":
|
524 |
+
iac *= 0.6
|
525 |
+
elif shading_type == "Internal Shading":
|
526 |
+
iac *= 0.8
|
527 |
shgc_dynamic = shgc * self.calculate_dynamic_shgc(glazing_type, cos_theta)
|
528 |
solar_heat_gain = comp.get('area', 0.0) * shgc_dynamic * I_t * iac / 1000 # kW
|
529 |
comp_result["solar_heat_gain"] = round(solar_heat_gain, 2)
|
530 |
comp_result["shgc_dynamic"] = round(shgc_dynamic, 2)
|
531 |
logger.info(f"Solar heat gain for {comp_result['component_id']} at {month}/{day}/{hour}: "
|
532 |
f"{solar_heat_gain:.2f} kW (area={comp.get('area', 0.0)}, shgc_dynamic={shgc_dynamic:.2f}, "
|
533 |
+
f"I_t={I_t:.2f}, iac={iac}, shading_type={shading_type})")
|
534 |
|
535 |
component_results.append(comp_result)
|
536 |
+
|
537 |
except Exception as e:
|
538 |
component_name = comp.get('name', 'unknown_component')
|
539 |
logger.error(f"Error processing component {component_name} at {month}/{day}/{hour}: {str(e)}")
|
540 |
continue
|
541 |
+
|
542 |
# Store results for this hour
|
543 |
result = {
|
544 |
"month": month,
|
|
|
552 |
"component_results": component_results
|
553 |
}
|
554 |
results.append(result)
|
555 |
+
|
556 |
return results
|