Upload climate_data.py
Browse files- data/climate_data.py +56 -89
data/climate_data.py
CHANGED
|
@@ -21,9 +21,6 @@ from datetime import datetime, timedelta
|
|
| 21 |
import re
|
| 22 |
import logging
|
| 23 |
import hashlib
|
| 24 |
-
import pytz
|
| 25 |
-
from typing import Dict, List
|
| 26 |
-
from timezonefinder import TimezoneFinder
|
| 27 |
|
| 28 |
# Set up logging
|
| 29 |
logging.basicConfig(level=logging.INFO)
|
|
@@ -92,8 +89,9 @@ class ClimateLocation:
|
|
| 92 |
_solar_cache: Dict[str, Dict[str, float]] # Cache for solar angle calculations
|
| 93 |
|
| 94 |
def __init__(self, epw_file: pd.DataFrame, typical_extreme_periods: Dict, ground_temperatures: Dict, albedo: float = 0.2, **kwargs):
|
|
|
|
| 95 |
self.id = kwargs.get("id")
|
| 96 |
-
self.country = kwargs.get("country")
|
| 97 |
self.state_province = kwargs.get("state_province", "N/A")
|
| 98 |
self.city = kwargs.get("city")
|
| 99 |
self.latitude = kwargs.get("latitude")
|
|
@@ -101,11 +99,10 @@ class ClimateLocation:
|
|
| 101 |
self.elevation = kwargs.get("elevation")
|
| 102 |
self.time_zone = kwargs.get("time_zone")
|
| 103 |
self.albedo = albedo
|
| 104 |
-
self.hourly_data = epw_file.iloc[8:].to_dict('records') # Assuming data starts at row 8
|
| 105 |
self.typical_extreme_periods = typical_extreme_periods
|
| 106 |
self.ground_temperatures = ground_temperatures
|
| 107 |
-
self.
|
| 108 |
-
self.
|
| 109 |
|
| 110 |
# Extract columns from EPW data
|
| 111 |
months = pd.to_numeric(epw_file[1], errors='coerce').values
|
|
@@ -209,110 +206,78 @@ class ClimateLocation:
|
|
| 209 |
return solar_time % 24
|
| 210 |
|
| 211 |
def _calculate_solar_angles(self, timestamp: datetime) -> Dict[str, float]:
|
| 212 |
-
"""
|
| 213 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
|
| 218 |
-
Returns:
|
| 219 |
-
Dict[str, float]: Dictionary with 'zenith' and 'azimuth' angles in degrees.
|
| 220 |
-
"""
|
| 221 |
-
# Make the timestamp timezone-aware
|
| 222 |
-
local_time = self.tz.localize(timestamp)
|
| 223 |
-
|
| 224 |
-
# Get solar position
|
| 225 |
solpos = pvlib.solarposition.get_solarposition(
|
| 226 |
-
time=
|
| 227 |
latitude=self.latitude,
|
| 228 |
longitude=self.longitude,
|
| 229 |
altitude=self.elevation,
|
| 230 |
-
method='nrel_numpy' #
|
|
|
|
| 231 |
)
|
| 232 |
-
|
| 233 |
-
return {
|
| 234 |
'zenith': float(solpos['zenith'].iloc[0]),
|
| 235 |
'azimuth': float(solpos['azimuth'].iloc[0])
|
| 236 |
}
|
|
|
|
|
|
|
| 237 |
|
| 238 |
-
def _calculate_solar_angles_batch(self, timestamps: List[datetime]) -> List[Dict[str, float]]:
|
| 239 |
-
"""
|
| 240 |
-
Compute solar zenith and azimuth angles for multiple timestamps (optimized for large datasets).
|
| 241 |
-
|
| 242 |
-
Args:
|
| 243 |
-
timestamps (List[datetime]): List of naive datetime objects to process.
|
| 244 |
-
|
| 245 |
-
Returns:
|
| 246 |
-
List[Dict[str, float]]: List of dictionaries with 'zenith' and 'azimuth' angles in degrees.
|
| 247 |
-
"""
|
| 248 |
-
# Localize all timestamps
|
| 249 |
-
local_times = [self.tz.localize(ts) for ts in timestamps]
|
| 250 |
-
|
| 251 |
-
# Get solar positions in a single vectorized call
|
| 252 |
-
solpos = pvlib.solarposition.get_solarposition(
|
| 253 |
-
time=pd.DatetimeIndex(local_times),
|
| 254 |
-
latitude=self.latitude,
|
| 255 |
-
longitude=self.longitude,
|
| 256 |
-
altitude=self.elevation,
|
| 257 |
-
method='nrel_numpy'
|
| 258 |
-
)
|
| 259 |
-
|
| 260 |
-
return [
|
| 261 |
-
{'zenith': float(solpos['zenith'].iloc[i]), 'azimuth': float(solpos['azimuth'].iloc[i])}
|
| 262 |
-
for i in range(len(timestamps))
|
| 263 |
-
]
|
| 264 |
-
|
| 265 |
def calculate_ground_reflection(self):
|
| 266 |
-
"""Calculate solar time, angles,
|
| 267 |
zenith_angles = []
|
| 268 |
for i, record in enumerate(self.hourly_data):
|
|
|
|
| 269 |
try:
|
| 270 |
-
|
| 271 |
-
if record['hour'] == 24:
|
| 272 |
-
# Handle hour=24 as 00:00 of the next day
|
| 273 |
-
if record['month'] == 12 and record['day'] == 31:
|
| 274 |
-
timestamp = datetime(2026, 1, 1, 0) # Next year if end of December
|
| 275 |
-
else:
|
| 276 |
-
timestamp = datetime(2025, record['month'], record['day']) + timedelta(days=1)
|
| 277 |
-
timestamp = timestamp.replace(hour=0)
|
| 278 |
-
else:
|
| 279 |
-
timestamp = datetime(2025, record['month'], record['day'], record['hour'])
|
| 280 |
except ValueError:
|
| 281 |
-
logger.warning(f"Invalid date at index {i}: {record['month']}/{record['day']} {record['hour']}")
|
| 282 |
continue
|
| 283 |
-
|
| 284 |
-
# Localize timestamp to fixed UTC+10
|
| 285 |
-
local_time = self.tz.localize(timestamp)
|
| 286 |
-
|
| 287 |
-
# Calculate solar angles using pvlib
|
| 288 |
-
solpos = pvlib.solarposition.get_solarposition(
|
| 289 |
-
time=pd.DatetimeIndex([local_time]),
|
| 290 |
-
latitude=self.latitude,
|
| 291 |
-
longitude=self.longitude,
|
| 292 |
-
altitude=self.elevation,
|
| 293 |
-
method='nrel_numpy'
|
| 294 |
-
)
|
| 295 |
-
record['solar_zenith'] = round(float(solpos['zenith'].iloc[0]), 1)
|
| 296 |
-
record['solar_azimuth'] = round(float(solpos['azimuth'].iloc[0]), 1)
|
| 297 |
-
zenith_angles.append(record['solar_zenith'])
|
| 298 |
-
|
| 299 |
# Calculate solar time
|
| 300 |
-
solar_time =
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
)
|
| 304 |
-
record['
|
| 305 |
-
|
|
|
|
|
|
|
| 306 |
# Calculate ground-reflected radiation using Perez model
|
| 307 |
dni = record['direct_normal_irradiance']
|
| 308 |
dhi = record['diffuse_horizontal_irradiance']
|
| 309 |
if dhi == 0:
|
|
|
|
| 310 |
record['ground_reflected_radiation'] = 0.0
|
| 311 |
else:
|
| 312 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
total_irradiance = pvlib.irradiance.get_total_irradiance(
|
| 314 |
-
surface_tilt=0,
|
| 315 |
-
surface_azimuth=0,
|
| 316 |
solar_zenith=record['solar_zenith'],
|
| 317 |
solar_azimuth=record['solar_azimuth'],
|
| 318 |
dni=dni,
|
|
@@ -322,19 +287,21 @@ class ClimateLocation:
|
|
| 322 |
albedo=self.albedo,
|
| 323 |
model='perez'
|
| 324 |
)
|
|
|
|
| 325 |
ground_reflected = total_irradiance['poa_ground_diffuse']
|
| 326 |
if isinstance(ground_reflected, pd.Series):
|
| 327 |
ground_reflected = ground_reflected.iloc[0]
|
|
|
|
| 328 |
record['ground_reflected_radiation'] = round(max(0.0, float(ground_reflected)), 1)
|
| 329 |
-
|
| 330 |
-
# Calculate solstice zenith angles
|
| 331 |
if zenith_angles:
|
| 332 |
self.solstice_zenith_angles = {
|
| 333 |
'summer': round(min(zenith_angles), 1), # Lowest zenith angle (highest sun)
|
| 334 |
'winter': round(max(zenith_angles), 1) # Highest zenith angle (lowest sun)
|
| 335 |
}
|
| 336 |
else:
|
| 337 |
-
logger.warning("No valid zenith angles calculated.")
|
| 338 |
self.solstice_zenith_angles = {'summer': None, 'winter': None}
|
| 339 |
|
| 340 |
def to_dict(self) -> Dict[str, Any]:
|
|
|
|
| 21 |
import re
|
| 22 |
import logging
|
| 23 |
import hashlib
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
# Set up logging
|
| 26 |
logging.basicConfig(level=logging.INFO)
|
|
|
|
| 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")
|
| 94 |
+
self.country = kwargs.get("country")
|
| 95 |
self.state_province = kwargs.get("state_province", "N/A")
|
| 96 |
self.city = kwargs.get("city")
|
| 97 |
self.latitude = kwargs.get("latitude")
|
|
|
|
| 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 = {} # Initialize empty, to be calculated later
|
| 106 |
|
| 107 |
# Extract columns from EPW data
|
| 108 |
months = pd.to_numeric(epw_file[1], errors='coerce').values
|
|
|
|
| 206 |
return solar_time % 24
|
| 207 |
|
| 208 |
def _calculate_solar_angles(self, timestamp: datetime) -> Dict[str, float]:
|
| 209 |
+
"""Compute solar zenith and azimuth angles using ASHRAE solar time."""
|
| 210 |
+
# Calculate solar time per ASHRAE
|
| 211 |
+
solar_time = self._calculate_solar_time(timestamp)
|
| 212 |
+
# Convert solar time back to a datetime object for pvlib
|
| 213 |
+
solar_hour = int(solar_time)
|
| 214 |
+
solar_minute = int((solar_time - solar_hour) * 60)
|
| 215 |
+
try:
|
| 216 |
+
solar_timestamp = datetime(
|
| 217 |
+
timestamp.year, timestamp.month, timestamp.day, solar_hour, solar_minute
|
| 218 |
+
)
|
| 219 |
+
except ValueError:
|
| 220 |
+
# Handle edge cases (e.g., solar time near midnight)
|
| 221 |
+
solar_timestamp = timestamp + timedelta(hours=solar_time - timestamp.hour)
|
| 222 |
|
| 223 |
+
# Generate cache key including solar time for precision
|
| 224 |
+
cache_key = hashlib.md5(
|
| 225 |
+
f"{solar_timestamp.strftime('%Y-%m-%d_%H:%M')}_{self.latitude}_{self.longitude}_{self.time_zone}".encode()
|
| 226 |
+
).hexdigest()
|
| 227 |
+
|
| 228 |
+
if cache_key in self._solar_cache:
|
| 229 |
+
return self._solar_cache[cache_key]
|
| 230 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
solpos = pvlib.solarposition.get_solarposition(
|
| 232 |
+
time=solar_timestamp,
|
| 233 |
latitude=self.latitude,
|
| 234 |
longitude=self.longitude,
|
| 235 |
altitude=self.elevation,
|
| 236 |
+
method='nrel_numpy', # Consistent method
|
| 237 |
+
delta_t=0 # Disable DST adjustments
|
| 238 |
)
|
| 239 |
+
result = {
|
|
|
|
| 240 |
'zenith': float(solpos['zenith'].iloc[0]),
|
| 241 |
'azimuth': float(solpos['azimuth'].iloc[0])
|
| 242 |
}
|
| 243 |
+
self._solar_cache[cache_key] = result
|
| 244 |
+
return result
|
| 245 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
def calculate_ground_reflection(self):
|
| 247 |
+
"""Calculate solar time, angles, ground-reflected radiation, and solstice zenith angles for hourly data."""
|
| 248 |
zenith_angles = []
|
| 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 |
+
zenith_angles.append(record['solar_zenith'])
|
| 265 |
+
|
| 266 |
# Calculate ground-reflected radiation using Perez model
|
| 267 |
dni = record['direct_normal_irradiance']
|
| 268 |
dhi = record['diffuse_horizontal_irradiance']
|
| 269 |
if dhi == 0:
|
| 270 |
+
# Skip Perez model if dhi is zero to avoid division by zero
|
| 271 |
record['ground_reflected_radiation'] = 0.0
|
| 272 |
else:
|
| 273 |
+
# Create a single timestamp index for pvlib
|
| 274 |
+
index = pd.DatetimeIndex([timestamp])
|
| 275 |
+
# Calculate extraterrestrial DNI for Perez model
|
| 276 |
+
dni_extra = pvlib.irradiance.get_extra_radiation(timestamp)
|
| 277 |
+
# Calculate total irradiance on a horizontal surface
|
| 278 |
total_irradiance = pvlib.irradiance.get_total_irradiance(
|
| 279 |
+
surface_tilt=0, # Horizontal surface
|
| 280 |
+
surface_azimuth=0, # Irrelevant for horizontal
|
| 281 |
solar_zenith=record['solar_zenith'],
|
| 282 |
solar_azimuth=record['solar_azimuth'],
|
| 283 |
dni=dni,
|
|
|
|
| 287 |
albedo=self.albedo,
|
| 288 |
model='perez'
|
| 289 |
)
|
| 290 |
+
# Handle both Series and scalar outputs for poa_ground_diffuse
|
| 291 |
ground_reflected = total_irradiance['poa_ground_diffuse']
|
| 292 |
if isinstance(ground_reflected, pd.Series):
|
| 293 |
ground_reflected = ground_reflected.iloc[0]
|
| 294 |
+
# Ensure the value is a float and handle potential NaN
|
| 295 |
record['ground_reflected_radiation'] = round(max(0.0, float(ground_reflected)), 1)
|
| 296 |
+
|
| 297 |
+
# Calculate solstice zenith angles based on max/min zenith angles
|
| 298 |
if zenith_angles:
|
| 299 |
self.solstice_zenith_angles = {
|
| 300 |
'summer': round(min(zenith_angles), 1), # Lowest zenith angle (highest sun)
|
| 301 |
'winter': round(max(zenith_angles), 1) # Highest zenith angle (lowest sun)
|
| 302 |
}
|
| 303 |
else:
|
| 304 |
+
logger.warning("No valid zenith angles calculated, solstice_zenith_angles remain empty.")
|
| 305 |
self.solstice_zenith_angles = {'summer': None, 'winter': None}
|
| 306 |
|
| 307 |
def to_dict(self) -> Dict[str, Any]:
|