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
|