mabuseif commited on
Commit
5098606
·
verified ·
1 Parent(s): f40219b

Upload climate_data.py

Browse files
Files changed (1) hide show
  1. 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") # Set country from kwargs
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.solstice_zenith_angles = {}
108
- self.tz = pytz.FixedOffset(int(self.time_zone * 60)) # Fixed offset in minutes
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
- Compute solar zenith and azimuth angles for a single timestamp using pvlib and timezone-aware datetime.
 
 
 
 
 
 
 
 
 
 
 
214
 
215
- Args:
216
- timestamp (datetime): Datetime object (naive, will be localized to the location's timezone).
 
 
 
 
 
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=pd.DatetimeIndex([local_time]),
227
  latitude=self.latitude,
228
  longitude=self.longitude,
229
  altitude=self.elevation,
230
- method='nrel_numpy' # High-accuracy method for ASHRAE compliance
 
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, and ground-reflected radiation for hourly data."""
267
  zenith_angles = []
268
  for i, record in enumerate(self.hourly_data):
 
269
  try:
270
- # Set timestamp to the end of the hour
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 = pvlib.solarposition.solar_time(
301
- time=pd.DatetimeIndex([local_time]),
302
- longitude=self.longitude
303
- )
304
- record['solar_time'] = round((solar_time.hour + solar_time.minute / 60.0) % 24, 2)
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
- dni_extra = pvlib.irradiance.get_extra_radiation(local_time)
 
 
 
 
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]: