Update data/climate_data.py
Browse files- data/climate_data.py +93 -86
data/climate_data.py
CHANGED
|
@@ -81,12 +81,13 @@ class ClimateLocation:
|
|
| 81 |
pressure: float # Mean atmospheric pressure (Pa)
|
| 82 |
direct_normal_irradiance: float # Mean DNI (W/m²)
|
| 83 |
diffuse_horizontal_irradiance: float # Mean DHI (W/m²)
|
|
|
|
| 84 |
hourly_data: List[Dict] # Hourly data for integration with main.py
|
| 85 |
typical_extreme_periods: Dict[str, Dict] # Typical/extreme periods (summer/winter)
|
| 86 |
ground_temperatures: Dict[str, List[float]] # Monthly ground temperatures by depth
|
| 87 |
-
solstice_zenith_angles: Dict[str, float] #
|
| 88 |
-
_solar_cache: Dict[str, Dict[str, float]] # Cache for solar
|
| 89 |
-
|
| 90 |
def __init__(self, epw_file: pd.DataFrame, typical_extreme_periods: Dict, ground_temperatures: Dict, albedo: float = 0.2, **kwargs):
|
| 91 |
"""Initialize ClimateLocation with EPW file data and header information."""
|
| 92 |
self.id = kwargs.get("id")
|
|
@@ -97,10 +98,11 @@ class ClimateLocation:
|
|
| 97 |
self.longitude = kwargs.get("longitude")
|
| 98 |
self.elevation = kwargs.get("elevation")
|
| 99 |
self.time_zone = kwargs.get("time_zone")
|
|
|
|
| 100 |
self.typical_extreme_periods = typical_extreme_periods
|
| 101 |
self.ground_temperatures = ground_temperatures
|
| 102 |
self._solar_cache = {}
|
| 103 |
-
self.
|
| 104 |
|
| 105 |
# Extract columns from EPW data
|
| 106 |
months = pd.to_numeric(epw_file[1], errors='coerce').values
|
|
@@ -120,9 +122,9 @@ class ClimateLocation:
|
|
| 120 |
if (wind_speed > 15).any():
|
| 121 |
logger.warning(f"High wind speeds detected: {wind_speed[wind_speed > 15].tolist()}")
|
| 122 |
|
| 123 |
-
# Filter irradiance outliers
|
| 124 |
-
direct_normal_irradiance = direct_normal_irradiance
|
| 125 |
-
diffuse_horizontal_irradiance = diffuse_horizontal_irradiance
|
| 126 |
if (direct_normal_irradiance > 1000).any():
|
| 127 |
logger.warning(f"High DNI values detected: {direct_normal_irradiance[direct_normal_irradiance > 1000].tolist()}")
|
| 128 |
if (diffuse_horizontal_irradiance > 600).any():
|
|
@@ -163,26 +165,11 @@ class ClimateLocation:
|
|
| 163 |
# Assign climate zone
|
| 164 |
self.climate_zone = ClimateData.assign_climate_zone(self.heating_degree_days, self.cooling_degree_days, np.nanmean(humidity))
|
| 165 |
|
| 166 |
-
# Calculate solstice zenith angles
|
| 167 |
-
self.solstice_zenith_angles = self._calculate_solstice_zenith_angles()
|
| 168 |
-
|
| 169 |
# Store hourly data with enhanced fields
|
| 170 |
self.hourly_data = []
|
| 171 |
for i in range(len(months)):
|
| 172 |
if np.isnan(months[i]) or np.isnan(days[i]) or np.isnan(hours[i]) or np.isnan(dry_bulb[i]):
|
| 173 |
continue # Skip records with missing critical fields
|
| 174 |
-
# Calculate solar time and angles
|
| 175 |
-
timestamp = datetime(2025, int(months[i]), int(days[i]), int(hours[i]) - 1) # Hour is 1-24, adjust to 0-23
|
| 176 |
-
solar_time = self._calculate_solar_time(timestamp)
|
| 177 |
-
solar_angles = self._calculate_solar_angles(timestamp)
|
| 178 |
-
|
| 179 |
-
# Handle DNI and DHI: set to 0 if missing or negative
|
| 180 |
-
dni = float(direct_normal_irradiance[i]) if not np.isnan(direct_normal_irradiance[i]) and direct_normal_irradiance[i] >= 0 else 0.0
|
| 181 |
-
dhi = float(diffuse_horizontal_irradiance[i]) if not np.isnan(diffuse_horizontal_irradiance[i]) and diffuse_horizontal_irradiance[i] >= 0 else 0.0
|
| 182 |
-
|
| 183 |
-
# Calculate ground-reflected radiation
|
| 184 |
-
ground_reflected = self.albedo * (dni * np.cos(np.radians(solar_angles['zenith'])) + dhi)
|
| 185 |
-
|
| 186 |
record = {
|
| 187 |
"month": int(months[i]),
|
| 188 |
"day": int(days[i]),
|
|
@@ -191,14 +178,10 @@ class ClimateLocation:
|
|
| 191 |
"relative_humidity": float(humidity[i]) if not np.isnan(humidity[i]) else 0.0,
|
| 192 |
"atmospheric_pressure": float(pressure[i]) if not np.isnan(pressure[i]) else self.pressure,
|
| 193 |
"global_horizontal_radiation": float(global_radiation[i]) if not np.isnan(global_radiation[i]) else 0.0,
|
| 194 |
-
"direct_normal_irradiance":
|
| 195 |
-
"diffuse_horizontal_irradiance":
|
| 196 |
"wind_speed": float(wind_speed[i]) if not np.isnan(wind_speed[i]) else 0.0,
|
| 197 |
-
"wind_direction": float(wind_direction[i]) if not np.isnan(wind_direction[i]) else 0.0
|
| 198 |
-
"solar_time": float(solar_time),
|
| 199 |
-
"solar_zenith": float(solar_angles['zenith']),
|
| 200 |
-
"solar_azimuth": float(solar_angles['azimuth']),
|
| 201 |
-
"ground_reflected_radiation": float(ground_reflected)
|
| 202 |
}
|
| 203 |
self.hourly_data.append(record)
|
| 204 |
|
|
@@ -206,27 +189,24 @@ class ClimateLocation:
|
|
| 206 |
st.warning(f"Hourly data has {len(self.hourly_data)} records instead of 8760. Some records may have been excluded due to missing data.")
|
| 207 |
|
| 208 |
def _calculate_solar_time(self, timestamp: datetime) -> float:
|
| 209 |
-
"""Calculate solar time
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
# Longitude correction (4 minutes per degree)
|
| 213 |
-
longitude_correction = (self.longitude - standard_meridian) * 4 / 60 # in hours
|
| 214 |
-
# Equation of time (simplified approximation, could use pvlib for precision)
|
| 215 |
day_of_year = timestamp.timetuple().tm_yday
|
| 216 |
B = (day_of_year - 1) * 360 / 365.242
|
| 217 |
-
E = 229.18 * (
|
| 218 |
-
|
| 219 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
local_std_time = timestamp.hour + timestamp.minute / 60
|
| 221 |
-
# Solar time
|
| 222 |
solar_time = local_std_time + longitude_correction + E
|
| 223 |
-
|
| 224 |
-
solar_time = solar_time % 24
|
| 225 |
-
return solar_time
|
| 226 |
|
| 227 |
def _calculate_solar_angles(self, timestamp: datetime) -> Dict[str, float]:
|
| 228 |
-
"""
|
| 229 |
-
# Create cache key
|
| 230 |
cache_key = hashlib.md5(
|
| 231 |
f"{timestamp.strftime('%Y-%m-%d_%H')}_{self.latitude}_{self.longitude}_{self.time_zone}".encode()
|
| 232 |
).hexdigest()
|
|
@@ -234,40 +214,59 @@ class ClimateLocation:
|
|
| 234 |
if cache_key in self._solar_cache:
|
| 235 |
return self._solar_cache[cache_key]
|
| 236 |
|
| 237 |
-
# Use pvlib for solar position
|
| 238 |
solpos = pvlib.solarposition.get_solarposition(
|
| 239 |
time=timestamp,
|
| 240 |
latitude=self.latitude,
|
| 241 |
longitude=self.longitude,
|
| 242 |
altitude=self.elevation
|
| 243 |
)
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
# Cache results
|
| 248 |
-
self._solar_cache[cache_key] = {
|
| 249 |
-
'zenith': zenith,
|
| 250 |
-
'azimuth': azimuth
|
| 251 |
}
|
| 252 |
-
|
|
|
|
| 253 |
|
| 254 |
def _calculate_solstice_zenith_angles(self) -> Dict[str, float]:
|
| 255 |
-
"""
|
| 256 |
-
|
| 257 |
-
datetime(2025, 12, 21, 12
|
| 258 |
-
datetime(2025, 6, 21, 12
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
for date in
|
| 262 |
solpos = pvlib.solarposition.get_solarposition(
|
| 263 |
time=date,
|
| 264 |
latitude=self.latitude,
|
| 265 |
longitude=self.longitude,
|
| 266 |
altitude=self.elevation
|
| 267 |
)
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
|
| 272 |
def to_dict(self) -> Dict[str, Any]:
|
| 273 |
"""Convert the climate location to a dictionary."""
|
|
@@ -343,8 +342,8 @@ class ClimateData:
|
|
| 343 |
"climate_zone", "heating_degree_days", "cooling_degree_days",
|
| 344 |
"winter_design_temp", "summer_design_temp_db", "summer_design_temp_wb",
|
| 345 |
"summer_daily_range", "wind_speed", "pressure",
|
| 346 |
-
"direct_normal_irradiance", "diffuse_horizontal_irradiance", "
|
| 347 |
-
"
|
| 348 |
]
|
| 349 |
|
| 350 |
for field in required_fields:
|
|
@@ -391,6 +390,13 @@ class ClimateData:
|
|
| 391 |
if not (0 <= data["albedo"] <= 1):
|
| 392 |
st.error(f"Validation failed: Albedo {data['albedo']} outside range")
|
| 393 |
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
if not data["hourly_data"] or len(data["hourly_data"]) < 8700:
|
| 395 |
st.error(f"Validation failed: Hourly data has {len(data['hourly_data'])} records, expected ~8760")
|
| 396 |
return False
|
|
@@ -428,16 +434,17 @@ class ClimateData:
|
|
| 428 |
if not (0 <= record["wind_direction"] <= 360):
|
| 429 |
st.error(f"Validation failed: Wind direction {record['wind_direction']} outside range")
|
| 430 |
return False
|
| 431 |
-
|
|
|
|
| 432 |
st.error(f"Validation failed: Solar time {record['solar_time']} outside range")
|
| 433 |
return False
|
| 434 |
-
if not (0 <= record["solar_zenith"] <= 180):
|
| 435 |
st.error(f"Validation failed: Solar zenith {record['solar_zenith']} outside range")
|
| 436 |
return False
|
| 437 |
-
if not (0 <= record["solar_azimuth"] <= 360):
|
| 438 |
st.error(f"Validation failed: Solar azimuth {record['solar_azimuth']} outside range")
|
| 439 |
return False
|
| 440 |
-
if not (0 <= record["ground_reflected_radiation"] <= 1200):
|
| 441 |
st.error(f"Validation failed: Ground reflected radiation {record['ground_reflected_radiation']} outside range")
|
| 442 |
return False
|
| 443 |
|
|
@@ -460,14 +467,6 @@ class ClimateData:
|
|
| 460 |
st.error(f"Validation failed: Invalid ground temperatures for depth {depth}")
|
| 461 |
return False
|
| 462 |
|
| 463 |
-
# Validate solstice zenith angles
|
| 464 |
-
if "solstice_zenith_angles" not in data or not all(key in data["solstice_zenith_angles"] for key in ["summer", "winter"]):
|
| 465 |
-
st.error("Validation failed: Missing or incomplete solstice zenith angles")
|
| 466 |
-
return False
|
| 467 |
-
if not (0 <= data["solstice_zenith_angles"]["summer"] <= 180 and 0 <= data["solstice_zenith_angles"]["winter"] <= 180):
|
| 468 |
-
st.error("Validation failed: Invalid solstice zenith angles")
|
| 469 |
-
return False
|
| 470 |
-
|
| 471 |
return True
|
| 472 |
|
| 473 |
@staticmethod
|
|
@@ -560,7 +559,7 @@ class ClimateData:
|
|
| 560 |
period_name = parts[2 + i*4]
|
| 561 |
period_type = parts[3 + i*4]
|
| 562 |
start_date = parts[4 + i*4].strip()
|
| 563 |
-
end_date =
|
| 564 |
if period_name in [
|
| 565 |
"Summer - Week Nearest Max Temperature For Period",
|
| 566 |
"Summer - Week Nearest Average Temperature For Period",
|
|
@@ -632,7 +631,7 @@ class ClimateData:
|
|
| 632 |
epw_file=epw_data,
|
| 633 |
typical_extreme_periods=typical_extreme_periods,
|
| 634 |
ground_temperatures=ground_temperatures,
|
| 635 |
-
albedo=albedo,
|
| 636 |
id=f"{country[:1].upper()}{city[:3].upper()}",
|
| 637 |
country=country,
|
| 638 |
state_province=state_province,
|
|
@@ -676,7 +675,7 @@ class ClimateData:
|
|
| 676 |
epw_file=epw_data,
|
| 677 |
typical_extreme_periods=climate_data_dict["typical_extreme_periods"],
|
| 678 |
ground_temperatures=climate_data_dict["ground_temperatures"],
|
| 679 |
-
albedo=climate_data_dict
|
| 680 |
id=climate_data_dict["id"],
|
| 681 |
country=climate_data_dict["country"],
|
| 682 |
state_province=climate_data_dict["state_province"],
|
|
@@ -686,12 +685,20 @@ class ClimateData:
|
|
| 686 |
elevation=climate_data_dict["elevation"],
|
| 687 |
time_zone=climate_data_dict["time_zone"]
|
| 688 |
)
|
| 689 |
-
# Override hourly_data to ensure consistency
|
| 690 |
location.hourly_data = climate_data_dict["hourly_data"]
|
| 691 |
-
location.solstice_zenith_angles = climate_data_dict
|
| 692 |
self.add_location(location)
|
| 693 |
st.info("Displaying previously extracted climate data.")
|
| 694 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 695 |
# Display tabs if location and epw_data are available
|
| 696 |
if location and epw_data is not None:
|
| 697 |
tab1, tab2, tab3, tab4, tab5 = st.tabs([
|
|
@@ -1107,7 +1114,7 @@ class ClimateData:
|
|
| 1107 |
epw_file=epw_data,
|
| 1108 |
typical_extreme_periods=loc_dict["typical_extreme_periods"],
|
| 1109 |
ground_temperatures=loc_dict["ground_temperatures"],
|
| 1110 |
-
albedo=loc_dict
|
| 1111 |
id=loc_dict["id"],
|
| 1112 |
country=loc_dict["country"],
|
| 1113 |
state_province=loc_dict["state_province"],
|
|
@@ -1117,12 +1124,12 @@ class ClimateData:
|
|
| 1117 |
elevation=loc_dict["elevation"],
|
| 1118 |
time_zone=loc_dict["time_zone"]
|
| 1119 |
)
|
| 1120 |
-
location.hourly_data = loc_dict["hourly_data"] #
|
| 1121 |
-
location.solstice_zenith_angles = loc_dict
|
| 1122 |
climate_data.add_location(location)
|
| 1123 |
return climate_data
|
| 1124 |
|
| 1125 |
if __name__ == "__main__":
|
| 1126 |
climate_data = ClimateData()
|
| 1127 |
session_state = {"building_info": {"country": "Australia", "city": "Geelong"}, "page": "Climate Data"}
|
| 1128 |
-
climate_data.display_climate_input(session_state, albedo=0.2)
|
|
|
|
| 81 |
pressure: float # Mean atmospheric pressure (Pa)
|
| 82 |
direct_normal_irradiance: float # Mean DNI (W/m²)
|
| 83 |
diffuse_horizontal_irradiance: float # Mean DHI (W/m²)
|
| 84 |
+
albedo: float # Surface reflectivity (0-1)
|
| 85 |
hourly_data: List[Dict] # Hourly data for integration with main.py
|
| 86 |
typical_extreme_periods: Dict[str, Dict] # Typical/extreme periods (summer/winter)
|
| 87 |
ground_temperatures: Dict[str, List[float]] # Monthly ground temperatures by depth
|
| 88 |
+
solstice_zenith_angles: Dict[str, float] # Zenith angles for summer/winter solstices
|
| 89 |
+
_solar_cache: Dict[str, Dict[str, float]] # Cache for solar angle calculations
|
| 90 |
+
|
| 91 |
def __init__(self, epw_file: pd.DataFrame, typical_extreme_periods: Dict, ground_temperatures: Dict, albedo: float = 0.2, **kwargs):
|
| 92 |
"""Initialize ClimateLocation with EPW file data and header information."""
|
| 93 |
self.id = kwargs.get("id")
|
|
|
|
| 98 |
self.longitude = kwargs.get("longitude")
|
| 99 |
self.elevation = kwargs.get("elevation")
|
| 100 |
self.time_zone = kwargs.get("time_zone")
|
| 101 |
+
self.albedo = albedo
|
| 102 |
self.typical_extreme_periods = typical_extreme_periods
|
| 103 |
self.ground_temperatures = ground_temperatures
|
| 104 |
self._solar_cache = {}
|
| 105 |
+
self.solstice_zenith_angles = self._calculate_solstice_zenith_angles()
|
| 106 |
|
| 107 |
# Extract columns from EPW data
|
| 108 |
months = pd.to_numeric(epw_file[1], errors='coerce').values
|
|
|
|
| 122 |
if (wind_speed > 15).any():
|
| 123 |
logger.warning(f"High wind speeds detected: {wind_speed[wind_speed > 15].tolist()}")
|
| 124 |
|
| 125 |
+
# Filter irradiance outliers and handle negative/missing values
|
| 126 |
+
direct_normal_irradiance = np.where((np.isnan(direct_normal_irradiance) | (direct_normal_irradiance < 0)), 0.0, direct_normal_irradiance)
|
| 127 |
+
diffuse_horizontal_irradiance = np.where((np.isnan(diffuse_horizontal_irradiance) | (diffuse_horizontal_irradiance < 0)), 0.0, diffuse_horizontal_irradiance)
|
| 128 |
if (direct_normal_irradiance > 1000).any():
|
| 129 |
logger.warning(f"High DNI values detected: {direct_normal_irradiance[direct_normal_irradiance > 1000].tolist()}")
|
| 130 |
if (diffuse_horizontal_irradiance > 600).any():
|
|
|
|
| 165 |
# Assign climate zone
|
| 166 |
self.climate_zone = ClimateData.assign_climate_zone(self.heating_degree_days, self.cooling_degree_days, np.nanmean(humidity))
|
| 167 |
|
|
|
|
|
|
|
|
|
|
| 168 |
# Store hourly data with enhanced fields
|
| 169 |
self.hourly_data = []
|
| 170 |
for i in range(len(months)):
|
| 171 |
if np.isnan(months[i]) or np.isnan(days[i]) or np.isnan(hours[i]) or np.isnan(dry_bulb[i]):
|
| 172 |
continue # Skip records with missing critical fields
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
record = {
|
| 174 |
"month": int(months[i]),
|
| 175 |
"day": int(days[i]),
|
|
|
|
| 178 |
"relative_humidity": float(humidity[i]) if not np.isnan(humidity[i]) else 0.0,
|
| 179 |
"atmospheric_pressure": float(pressure[i]) if not np.isnan(pressure[i]) else self.pressure,
|
| 180 |
"global_horizontal_radiation": float(global_radiation[i]) if not np.isnan(global_radiation[i]) else 0.0,
|
| 181 |
+
"direct_normal_irradiance": float(direct_normal_irradiance[i]),
|
| 182 |
+
"diffuse_horizontal_irradiance": float(diffuse_horizontal_irradiance[i]),
|
| 183 |
"wind_speed": float(wind_speed[i]) if not np.isnan(wind_speed[i]) else 0.0,
|
| 184 |
+
"wind_direction": float(wind_direction[i]) if not np.isnan(wind_direction[i]) else 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
}
|
| 186 |
self.hourly_data.append(record)
|
| 187 |
|
|
|
|
| 189 |
st.warning(f"Hourly data has {len(self.hourly_data)} records instead of 8760. Some records may have been excluded due to missing data.")
|
| 190 |
|
| 191 |
def _calculate_solar_time(self, timestamp: datetime) -> float:
|
| 192 |
+
"""Calculate solar time using ASHRAE Fundamentals, Chapter 14, Section 14.7."""
|
| 193 |
+
standard_meridian = self.time_zone * 15
|
| 194 |
+
longitude_correction = (self.longitude - standard_meridian) * 4 / 60
|
|
|
|
|
|
|
|
|
|
| 195 |
day_of_year = timestamp.timetuple().tm_yday
|
| 196 |
B = (day_of_year - 1) * 360 / 365.242
|
| 197 |
+
E = 229.18 * (
|
| 198 |
+
0.000075 +
|
| 199 |
+
0.001868 * np.cos(np.radians(B)) -
|
| 200 |
+
0.032077 * np.sin(np.radians(B)) -
|
| 201 |
+
0.014615 * np.cos(np.radians(2 * B)) -
|
| 202 |
+
0.04089 * np.sin(np.radians(2 * B))
|
| 203 |
+
) / 60
|
| 204 |
local_std_time = timestamp.hour + timestamp.minute / 60
|
|
|
|
| 205 |
solar_time = local_std_time + longitude_correction + E
|
| 206 |
+
return solar_time % 24
|
|
|
|
|
|
|
| 207 |
|
| 208 |
def _calculate_solar_angles(self, timestamp: datetime) -> Dict[str, float]:
|
| 209 |
+
"""Compute solar zenith and azimuth angles with caching."""
|
|
|
|
| 210 |
cache_key = hashlib.md5(
|
| 211 |
f"{timestamp.strftime('%Y-%m-%d_%H')}_{self.latitude}_{self.longitude}_{self.time_zone}".encode()
|
| 212 |
).hexdigest()
|
|
|
|
| 214 |
if cache_key in self._solar_cache:
|
| 215 |
return self._solar_cache[cache_key]
|
| 216 |
|
|
|
|
| 217 |
solpos = pvlib.solarposition.get_solarposition(
|
| 218 |
time=timestamp,
|
| 219 |
latitude=self.latitude,
|
| 220 |
longitude=self.longitude,
|
| 221 |
altitude=self.elevation
|
| 222 |
)
|
| 223 |
+
result = {
|
| 224 |
+
'zenith': float(solpos['zenith'].iloc[0]),
|
| 225 |
+
'azimuth': float(solpos['azimuth'].iloc[0])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
}
|
| 227 |
+
self._solar_cache[cache_key] = result
|
| 228 |
+
return result
|
| 229 |
|
| 230 |
def _calculate_solstice_zenith_angles(self) -> Dict[str, float]:
|
| 231 |
+
"""Compute zenith angles for summer and winter solstices at noon."""
|
| 232 |
+
solstices = {
|
| 233 |
+
'summer': datetime(2025, 12, 21, 12, 0),
|
| 234 |
+
'winter': datetime(2025, 6, 21, 12, 0)
|
| 235 |
+
}
|
| 236 |
+
result = {}
|
| 237 |
+
for season, date in solstices.items():
|
| 238 |
solpos = pvlib.solarposition.get_solarposition(
|
| 239 |
time=date,
|
| 240 |
latitude=self.latitude,
|
| 241 |
longitude=self.longitude,
|
| 242 |
altitude=self.elevation
|
| 243 |
)
|
| 244 |
+
result[season] = round(float(solpos['zenith'].iloc[0]), 1)
|
| 245 |
+
return result
|
| 246 |
+
|
| 247 |
+
def calculate_ground_reflection(self):
|
| 248 |
+
"""Calculate solar time, angles, and ground-reflected radiation for hourly data."""
|
| 249 |
+
for i, record in enumerate(self.hourly_data):
|
| 250 |
+
# Create timestamp (adjust hour from 1-24 to 0-23)
|
| 251 |
+
try:
|
| 252 |
+
timestamp = datetime(2025, record['month'], record['day'], record['hour'] - 1)
|
| 253 |
+
except ValueError:
|
| 254 |
+
logger.warning(f"Invalid date in hourly data at index {i}: {record['month']}/{record['day']} {record['hour']}")
|
| 255 |
+
continue
|
| 256 |
+
|
| 257 |
+
# Calculate solar time
|
| 258 |
+
record['solar_time'] = round(self._calculate_solar_time(timestamp), 2)
|
| 259 |
+
|
| 260 |
+
# Calculate solar angles
|
| 261 |
+
solar_angles = self._calculate_solar_angles(timestamp)
|
| 262 |
+
record['solar_zenith'] = round(solar_angles['zenith'], 1)
|
| 263 |
+
record['solar_azimuth'] = round(solar_angles['azimuth'], 1)
|
| 264 |
+
|
| 265 |
+
# Calculate ground-reflected radiation
|
| 266 |
+
dni = record['direct_normal_irradiance']
|
| 267 |
+
dhi = record['diffuse_horizontal_irradiance']
|
| 268 |
+
ground_reflected = self.albedo * (dni * np.cos(np.radians(solar_angles['zenith'])) + dhi)
|
| 269 |
+
record['ground_reflected_radiation'] = round(max(0.0, ground_reflected), 1)
|
| 270 |
|
| 271 |
def to_dict(self) -> Dict[str, Any]:
|
| 272 |
"""Convert the climate location to a dictionary."""
|
|
|
|
| 342 |
"climate_zone", "heating_degree_days", "cooling_degree_days",
|
| 343 |
"winter_design_temp", "summer_design_temp_db", "summer_design_temp_wb",
|
| 344 |
"summer_daily_range", "wind_speed", "pressure",
|
| 345 |
+
"direct_normal_irradiance", "diffuse_horizontal_irradiance", "albedo",
|
| 346 |
+
"hourly_data", "solstice_zenith_angles"
|
| 347 |
]
|
| 348 |
|
| 349 |
for field in required_fields:
|
|
|
|
| 390 |
if not (0 <= data["albedo"] <= 1):
|
| 391 |
st.error(f"Validation failed: Albedo {data['albedo']} outside range")
|
| 392 |
return False
|
| 393 |
+
if not all(k in data["solstice_zenith_angles"] for k in ["summer", "winter"]):
|
| 394 |
+
st.error("Validation failed: Missing summer or winter solstice zenith angles")
|
| 395 |
+
return False
|
| 396 |
+
if not all(0 <= data["solstice_zenith_angles"][k] <= 180 for k in ["summer", "winter"]):
|
| 397 |
+
st.error("Validation failed: Solstice zenith angles outside range 0-180")
|
| 398 |
+
return False
|
| 399 |
+
|
| 400 |
if not data["hourly_data"] or len(data["hourly_data"]) < 8700:
|
| 401 |
st.error(f"Validation failed: Hourly data has {len(data['hourly_data'])} records, expected ~8760")
|
| 402 |
return False
|
|
|
|
| 434 |
if not (0 <= record["wind_direction"] <= 360):
|
| 435 |
st.error(f"Validation failed: Wind direction {record['wind_direction']} outside range")
|
| 436 |
return False
|
| 437 |
+
# Validate new hourly fields if present
|
| 438 |
+
if "solar_time" in record and not (0 <= record["solar_time"] < 24):
|
| 439 |
st.error(f"Validation failed: Solar time {record['solar_time']} outside range")
|
| 440 |
return False
|
| 441 |
+
if "solar_zenith" in record and not (0 <= record["solar_zenith"] <= 180):
|
| 442 |
st.error(f"Validation failed: Solar zenith {record['solar_zenith']} outside range")
|
| 443 |
return False
|
| 444 |
+
if "solar_azimuth" in record and not (0 <= record["solar_azimuth"] <= 360):
|
| 445 |
st.error(f"Validation failed: Solar azimuth {record['solar_azimuth']} outside range")
|
| 446 |
return False
|
| 447 |
+
if "ground_reflected_radiation" in record and not (0 <= record["ground_reflected_radiation"] <= 1200):
|
| 448 |
st.error(f"Validation failed: Ground reflected radiation {record['ground_reflected_radiation']} outside range")
|
| 449 |
return False
|
| 450 |
|
|
|
|
| 467 |
st.error(f"Validation failed: Invalid ground temperatures for depth {depth}")
|
| 468 |
return False
|
| 469 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
return True
|
| 471 |
|
| 472 |
@staticmethod
|
|
|
|
| 559 |
period_name = parts[2 + i*4]
|
| 560 |
period_type = parts[3 + i*4]
|
| 561 |
start_date = parts[4 + i*4].strip()
|
| 562 |
+
end_date = parts[5 + i*4].strip()
|
| 563 |
if period_name in [
|
| 564 |
"Summer - Week Nearest Max Temperature For Period",
|
| 565 |
"Summer - Week Nearest Average Temperature For Period",
|
|
|
|
| 631 |
epw_file=epw_data,
|
| 632 |
typical_extreme_periods=typical_extreme_periods,
|
| 633 |
ground_temperatures=ground_temperatures,
|
| 634 |
+
albedo=albedo,
|
| 635 |
id=f"{country[:1].upper()}{city[:3].upper()}",
|
| 636 |
country=country,
|
| 637 |
state_province=state_province,
|
|
|
|
| 675 |
epw_file=epw_data,
|
| 676 |
typical_extreme_periods=climate_data_dict["typical_extreme_periods"],
|
| 677 |
ground_temperatures=climate_data_dict["ground_temperatures"],
|
| 678 |
+
albedo=climate_data_dict.get("albedo", albedo),
|
| 679 |
id=climate_data_dict["id"],
|
| 680 |
country=climate_data_dict["country"],
|
| 681 |
state_province=climate_data_dict["state_province"],
|
|
|
|
| 685 |
elevation=climate_data_dict["elevation"],
|
| 686 |
time_zone=climate_data_dict["time_zone"]
|
| 687 |
)
|
| 688 |
+
# Override hourly_data and solstice_zenith_angles to ensure consistency
|
| 689 |
location.hourly_data = climate_data_dict["hourly_data"]
|
| 690 |
+
location.solstice_zenith_angles = climate_data_dict.get("solstice_zenith_angles", location._calculate_solstice_zenith_angles())
|
| 691 |
self.add_location(location)
|
| 692 |
st.info("Displaying previously extracted climate data.")
|
| 693 |
|
| 694 |
+
# Add button for calculating ground reflection radiation
|
| 695 |
+
if location and epw_data is not None:
|
| 696 |
+
if st.button("Calculate Ground Reflection Radiation"):
|
| 697 |
+
with st.spinner("Calculating solar time, angles, and ground-reflected radiation... Please wait."):
|
| 698 |
+
location.calculate_ground_reflection()
|
| 699 |
+
session_state["climate_data"] = location.to_dict()
|
| 700 |
+
st.success("Ground reflection calculations completed!")
|
| 701 |
+
|
| 702 |
# Display tabs if location and epw_data are available
|
| 703 |
if location and epw_data is not None:
|
| 704 |
tab1, tab2, tab3, tab4, tab5 = st.tabs([
|
|
|
|
| 1114 |
epw_file=epw_data,
|
| 1115 |
typical_extreme_periods=loc_dict["typical_extreme_periods"],
|
| 1116 |
ground_temperatures=loc_dict["ground_temperatures"],
|
| 1117 |
+
albedo=loc_dict.get("albedo", 0.2),
|
| 1118 |
id=loc_dict["id"],
|
| 1119 |
country=loc_dict["country"],
|
| 1120 |
state_province=loc_dict["state_province"],
|
|
|
|
| 1124 |
elevation=loc_dict["elevation"],
|
| 1125 |
time_zone=loc_dict["time_zone"]
|
| 1126 |
)
|
| 1127 |
+
location.hourly_data = loc_dict["hourly_data"] # Ensure consistency
|
| 1128 |
+
location.solstice_zenith_angles = loc_dict.get("solstice_zenith_angles", location._calculate_solstice_zenith_angles())
|
| 1129 |
climate_data.add_location(location)
|
| 1130 |
return climate_data
|
| 1131 |
|
| 1132 |
if __name__ == "__main__":
|
| 1133 |
climate_data = ClimateData()
|
| 1134 |
session_state = {"building_info": {"country": "Australia", "city": "Geelong"}, "page": "Climate Data"}
|
| 1135 |
+
climate_data.display_climate_input(session_state, albedo=0.2)
|