weather_utils.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. import math
  2. import numpy as np
  3. import pandas as pd
  4. from typing import Tuple
  5. def calculate_wind_speed_direction(u: float, v: float) -> Tuple[float, float]:
  6. """
  7. Calculate wind speed and direction from u and v wind components.
  8. Args:
  9. u (float): East-west wind component (positive for eastward wind)
  10. v (float): North-south wind component (positive for northward wind)
  11. Returns:
  12. Tuple[float, float]: Wind speed (m/s) and direction (degrees from north, clockwise)
  13. Note:
  14. - Wind direction is in meteorological convention (degrees from north, clockwise)
  15. - Wind direction indicates where the wind is coming FROM (not going to)
  16. - 0 degrees is from the north
  17. - 90 degrees is from the east
  18. - 180 degrees is from the south
  19. - 270 degrees is from the west
  20. """
  21. # Calculate wind speed
  22. wind_speed = np.sqrt(u**2 + v**2)
  23. # Calculate wind direction using meteorological convention
  24. # arctan2(x, y) gives the angle in radians between the positive x-axis and the point (x, y)
  25. # For wind direction (where wind is coming FROM), we need to use negative components
  26. # and adjust the angle to get the meteorological convention
  27. wind_direction_rad = np.arctan2(-u, -v)
  28. wind_direction_deg = np.degrees(wind_direction_rad)
  29. wind_direction_deg = (wind_direction_deg + 360) % 360
  30. return wind_speed, wind_direction_deg
  31. def calculate_wind_components(speed: float, direction: float) -> Tuple[float, float]:
  32. """
  33. Calculate u and v wind components from wind speed and direction.
  34. Args:
  35. speed (float): Wind speed in m/s
  36. direction (float): Wind direction in degrees from north, clockwise (where wind is coming FROM)
  37. Returns:
  38. Tuple[float, float]: u (east-west) and v (north-south) wind components
  39. Note:
  40. - Positive u means eastward wind
  41. - Positive v means northward wind
  42. - This is the inverse operation of calculate_wind_speed_direction
  43. """
  44. # Convert direction from degrees to radians
  45. # Since direction is where wind is coming FROM, we need to add 180 degrees to get where it's going TO
  46. direction_rad = np.radians((direction + 180) % 360)
  47. # Calculate components
  48. u = -speed * np.sin(direction_rad)
  49. v = -speed * np.cos(direction_rad)
  50. return u, v
  51. def interpolate_weather_attributes(df: pd.DataFrame, time_col: str, time_interval: str='15min', method: str='cubic') -> pd.DataFrame:
  52. """
  53. Interpolate weather attributes to a uniform time interval using specified method.
  54. This function handles cases where input data has varying frequencies
  55. (e.g., first 72h hourly, next 72h 3-hourly, last 72h 6-hourly) and
  56. creates a uniform time grid with interpolation.
  57. Args:
  58. df (pd.DataFrame): Input DataFrame with weather attributes
  59. time_col (str): Name of the time column
  60. time_interval (str): Target time interval for interpolation (e.g., '15min', '30min', '1h')
  61. method (str): Interpolation method to use ('linear' or 'cubic')
  62. Returns:
  63. pd.DataFrame: DataFrame with interpolated weather attributes at uniform intervals
  64. Example:
  65. df_interp = interpolate_weather_attributes(df, 'time', '15min', method='cubic')
  66. """
  67. if df.empty:
  68. return df.copy()
  69. if method not in ['linear', 'cubic']:
  70. raise ValueError("Method must be either 'linear' or 'cubic'")
  71. # Make a copy to avoid modifying the original DataFrame
  72. df_work = df.copy()
  73. # Ensure time column is datetime
  74. if not pd.api.types.is_datetime64_any_dtype(df_work[time_col]):
  75. df_work[time_col] = pd.to_datetime(df_work[time_col])
  76. # Sort by time to ensure proper interpolation
  77. df_work = df_work.sort_values(time_col).reset_index(drop=True)
  78. # Remove any duplicate timestamps, keeping the first occurrence
  79. df_work = df_work.drop_duplicates(subset=[time_col], keep='first')
  80. # Get the time range
  81. start_time = df_work[time_col].min()
  82. end_time = df_work[time_col].max()
  83. # Create uniform time grid
  84. time_grid = pd.date_range(start=start_time, end=end_time, freq=time_interval)
  85. # Create new DataFrame with uniform time grid
  86. df_uniform = pd.DataFrame({time_col: time_grid})
  87. # Set time as index for both DataFrames to facilitate interpolation
  88. df_work_indexed = df_work.set_index(time_col)
  89. df_uniform_indexed = df_uniform.set_index(time_col)
  90. # Get all columns except the time column for interpolation
  91. value_columns = [col for col in df_work.columns if col != time_col]
  92. # Perform interpolation for each weather attribute
  93. for col in value_columns:
  94. if pd.api.types.is_numeric_dtype(df_work_indexed[col]):
  95. # For numeric columns, use specified interpolation method
  96. # First, reindex to include all target time points
  97. series_reindexed = df_work_indexed[col].reindex(
  98. df_work_indexed.index.union(df_uniform_indexed.index)
  99. ).sort_index()
  100. # Interpolate using specified method
  101. series_interpolated = series_reindexed.interpolate(method=method)
  102. # Extract values at uniform time grid points
  103. df_uniform[col] = series_interpolated.reindex(df_uniform_indexed.index).values
  104. else:
  105. # For non-numeric columns, use forward fill
  106. series_reindexed = df_work_indexed[col].reindex(
  107. df_work_indexed.index.union(df_uniform_indexed.index)
  108. ).sort_index()
  109. series_filled = series_reindexed.ffill()
  110. df_uniform[col] = series_filled.reindex(df_uniform_indexed.index).values
  111. # Handle any remaining NaN values at the beginning or end
  112. # Forward fill first, then backward fill
  113. df_uniform = df_uniform.ffill().bfill()
  114. return df_uniform
  115. def nearest_position(stn_lat: float, stn_lon: float, lat2d: np.ndarray, lon2d: np.ndarray) -> Tuple[int, int]:
  116. """
  117. Find the nearest grid point to a given station location using great circle distance.
  118. Args:
  119. stn_lat (float): Station latitude in degrees
  120. stn_lon (float): Station longitude in degrees
  121. lat2d (np.ndarray): 2D array of grid latitudes
  122. lon2d (np.ndarray): 2D array of grid longitudes
  123. Returns:
  124. Tuple[int, int]: (row, col) indices of the nearest grid point
  125. Note:
  126. Uses great circle distance calculation which is more accurate for geographic coordinates
  127. than simple Euclidean distance.
  128. """
  129. # Convert to radians for distance calculation
  130. stn_lat_rad = np.radians(stn_lat)
  131. stn_lon_rad = np.radians(stn_lon)
  132. lat2d_rad = np.radians(lat2d)
  133. lon2d_rad = np.radians(lon2d)
  134. # Calculate great circle distance using haversine formula
  135. dlat = lat2d_rad - stn_lat_rad
  136. dlon = lon2d_rad - stn_lon_rad
  137. a = np.sin(dlat/2)**2 + np.cos(stn_lat_rad) * np.cos(lat2d_rad) * np.sin(dlon/2)**2
  138. c = 2 * np.arcsin(np.sqrt(a))
  139. distance = 6371 * c # Earth's radius in km
  140. # Find indices of minimum distance
  141. min_idx = np.unravel_index(np.argmin(distance), distance.shape)
  142. return min_idx
  143. def calculate_sunrise_sunset(date, latitude, longitude):
  144. """
  145. Calculate sunrise and sunset times for a given date and location.
  146. Based on the equations from NOAA:
  147. https://www.esrl.noaa.gov/gmd/grad/solcalc/
  148. Args:
  149. date: Date to calculate sunrise/sunset for (in Beijing time)
  150. latitude: Latitude in degrees
  151. longitude: Longitude in degrees
  152. Returns:
  153. Tuple of (sunrise_hour, sunset_hour) in Beijing time (decimal hours)
  154. """
  155. # Convert latitude and longitude to radians
  156. lat_rad = math.radians(latitude)
  157. # Calculate day of year
  158. day_of_year = date.timetuple().tm_yday
  159. # Calculate solar declination angle (in radians)
  160. declination = 0.4093 * math.sin(2 * math.pi / 365 * (day_of_year - 81))
  161. # Calculate equation of time (in minutes)
  162. B = 2 * math.pi / 365 * (day_of_year - 81)
  163. time_eq = 9.87 * math.sin(2 * B) - 7.53 * math.cos(B) - 1.5 * math.sin(B)
  164. # Calculate solar hour angle at sunrise/sunset (in radians)
  165. # Using -0.83 degrees as the solar elevation angle at sunrise/sunset
  166. solar_elevation = math.radians(-0.83)
  167. ha = math.acos(
  168. (math.sin(solar_elevation) - math.sin(lat_rad) * math.sin(declination)) /
  169. (math.cos(lat_rad) * math.cos(declination))
  170. )
  171. # Convert hour angle to hours
  172. ha_hours = math.degrees(ha) / 15.0
  173. # Calculate sunrise and sunset (in local solar time, hours)
  174. sunrise_local = 12.0 - ha_hours - time_eq / 60.0
  175. sunset_local = 12.0 + ha_hours - time_eq / 60.0
  176. # Convert local solar time to Beijing time (UTC+8, 120°E)
  177. # Beijing time = local solar time + (120 - longitude)/15
  178. longitude_correction = (120.0 - longitude) / 15.0
  179. sunrise_beijing = (sunrise_local + longitude_correction) % 24
  180. sunset_beijing = (sunset_local + longitude_correction) % 24
  181. return sunrise_beijing, sunset_beijing
  182. def solar_elevation_angle(date_time, latitude, longitude, timezone_longitude=120):
  183. """
  184. Calculate solar elevation angle in degrees.
  185. Args:
  186. date_time: datetime object
  187. latitude: latitude in degrees (-90 to 90)
  188. longitude: longitude in degrees (-180 to 180)
  189. timezone_longitude: timezone longitude in degrees (-180 to 180)
  190. Returns:
  191. Solar elevation angle in degrees
  192. """
  193. # Input validation
  194. if not -90 <= latitude <= 90:
  195. raise ValueError("Latitude must be between -90 and 90 degrees")
  196. if not -180 <= longitude <= 180:
  197. raise ValueError("Longitude must be between -180 and 180 degrees")
  198. if not -180 <= timezone_longitude <= 180:
  199. raise ValueError("Timezone longitude must be between -180 and 180 degrees")
  200. # Time information
  201. n = date_time.timetuple().tm_yday
  202. decimal_hours = date_time.hour + date_time.minute / 60 + date_time.second / 3600
  203. # Calculate solar declination and hour angle
  204. delta = solar_declination(n)
  205. H = hour_angle(decimal_hours, longitude, timezone_longitude, n)
  206. # Calculate elevation angle
  207. phi = latitude
  208. sin_h = math.sin(math.radians(phi)) * math.sin(math.radians(delta)) + \
  209. math.cos(math.radians(phi)) * math.cos(math.radians(delta)) * math.cos(math.radians(H))
  210. h = math.degrees(math.asin(sin_h))
  211. # Apply atmospheric refraction correction
  212. h += atmospheric_refraction(h)
  213. return h
  214. def solar_declination(n):
  215. """Calculate solar declination angle in degrees"""
  216. return 23.45 * math.sin(math.radians(360 / 365 * (n - 81)))
  217. def hour_angle(local_time, longitude, timezone_longitude, n):
  218. """Calculate hour angle in degrees"""
  219. eot = equation_of_time(n)
  220. lst = local_time + (timezone_longitude - longitude) * 4 / 60 + eot / 60
  221. return 15 * (lst - 12)
  222. def equation_of_time(n):
  223. """Calculate the equation of time in minutes"""
  224. B = 360 / 365 * (n - 81)
  225. return 9.87 * math.sin(2 * math.radians(B)) - 7.53 * math.cos(math.radians(B)) - 1.5 * math.sin(math.radians(B))
  226. def atmospheric_refraction(elevation):
  227. """
  228. Calculate atmospheric refraction correction in degrees using Bennett's formula.
  229. Atmospheric refraction causes celestial objects to appear higher in the sky
  230. than their true geometric position. This effect is strongest at the horizon
  231. and decreases with increasing elevation angle.
  232. Args:
  233. elevation: Apparent elevation angle in degrees
  234. Returns:
  235. Atmospheric refraction correction in degrees (always positive)
  236. Reference:
  237. Bennett, G.G. (1982). "The Calculation of Astronomical Refraction in Marine Navigation"
  238. Journal of Navigation, 35(2), 255-259
  239. """
  240. # No refraction for objects below the horizon (accounting for refraction itself)
  241. if elevation < -0.575:
  242. return 0
  243. # For very high elevations, refraction is negligible
  244. if elevation > 85:
  245. return 0
  246. # Bennett's formula for atmospheric refraction
  247. # R = 1.02 / tan(h + 10.3/(h + 5.11)) arcminutes
  248. # Convert to degrees by dividing by 60
  249. # Handle low elevations with polynomial approximation for better accuracy
  250. if elevation < 15:
  251. # Sæmundsson's formula for low elevations (more accurate near horizon)
  252. # R in arcseconds = 1735 + h*(-518.2 + h*(103.4 + h*(-12.79 + h*0.711)))
  253. # where h is in degrees
  254. h = elevation
  255. refraction_arcsec = 1735 + h * (-518.2 + h * (103.4 + h * (-12.79 + h * 0.711)))
  256. refraction_deg = refraction_arcsec / 3600 # Convert arcseconds to degrees
  257. else:
  258. # Bennett's formula for higher elevations
  259. h = elevation
  260. refraction_arcmin = 1.02 / math.tan(math.radians(h + 10.3 / (h + 5.11)))
  261. refraction_deg = refraction_arcmin / 60 # Convert arcminutes to degrees
  262. return refraction_deg
  263. def solar_azimuth_angle(date_time, latitude, longitude, timezone_longitude=120):
  264. """
  265. Calculate solar azimuth angle in degrees.
  266. The azimuth angle is measured clockwise from true North:
  267. - 0° = North
  268. - 90° = East
  269. - 180° = South
  270. - 270° = West
  271. Args:
  272. date_time: datetime object
  273. latitude: latitude in degrees (-90 to 90)
  274. longitude: longitude in degrees (-180 to 180)
  275. timezone_longitude: timezone longitude in degrees (-180 to 180)
  276. Returns:
  277. Solar azimuth angle in degrees (0-360)
  278. Reference:
  279. Uses standard solar position equations from NOAA and astronomical texts
  280. """
  281. # Input validation
  282. if not -90 <= latitude <= 90:
  283. raise ValueError("Latitude must be between -90 and 90 degrees")
  284. if not -180 <= longitude <= 180:
  285. raise ValueError("Longitude must be between -180 and 180 degrees")
  286. if not -180 <= timezone_longitude <= 180:
  287. raise ValueError("Timezone longitude must be between -180 and 180 degrees")
  288. # Time information
  289. n = date_time.timetuple().tm_yday
  290. decimal_hours = date_time.hour + date_time.minute / 60 + date_time.second / 3600
  291. # Calculate solar declination and hour angle
  292. delta = solar_declination(n) # Solar declination in degrees
  293. H = hour_angle(decimal_hours, longitude, timezone_longitude, n) # Hour angle in degrees
  294. # Convert to radians for calculation
  295. phi_rad = math.radians(latitude)
  296. delta_rad = math.radians(delta)
  297. H_rad = math.radians(H)
  298. # Calculate azimuth using atan2 for correct quadrant handling
  299. # Formula: A = atan2(sin(H), cos(H) × sin(φ) - tan(δ) × cos(φ))
  300. # This gives azimuth measured from South, positive towards West
  301. # Calculate elevation angle for reference
  302. sin_h = math.sin(phi_rad) * math.sin(delta_rad) + \
  303. math.cos(phi_rad) * math.cos(delta_rad) * math.cos(H_rad)
  304. # Calculate azimuth components
  305. numerator = math.sin(H_rad)
  306. denominator = math.cos(H_rad) * math.sin(phi_rad) - math.tan(delta_rad) * math.cos(phi_rad)
  307. # Calculate azimuth from South
  308. azimuth_rad = math.atan2(numerator, denominator)
  309. azimuth_deg = math.degrees(azimuth_rad)
  310. # Convert from "from South" to "from North" convention
  311. # Azimuth from South: 0° = South, positive towards West
  312. # We want: 0° = North, positive clockwise (East)
  313. azimuth_from_north = (azimuth_deg + 180) % 360
  314. return azimuth_from_north
  315. def calculate_clear_sky_radiation(date_time, latitude, longitude, timezone_longitude=120):
  316. """
  317. Calculate theoretical clear-sky solar radiation using simplified Bird Clear Sky Model.
  318. This represents the maximum possible radiation for given sun position under
  319. perfectly clear atmospheric conditions.
  320. Args:
  321. date_time: datetime object
  322. latitude: latitude in degrees (-90 to 90)
  323. longitude: longitude in degrees (-180 to 180)
  324. timezone_longitude: timezone longitude in degrees (-180 to 180)
  325. Returns:
  326. Clear-sky radiation in W/m² (0 if sun is below horizon)
  327. Reference:
  328. Simplified Bird Clear Sky Model
  329. R.E. Bird and R.L. Hulstrom (1981)
  330. """
  331. # Calculate solar elevation angle (already includes atmospheric refraction)
  332. elevation = solar_elevation_angle(date_time, latitude, longitude, timezone_longitude)
  333. # If sun is below horizon, no radiation
  334. if elevation <= 0:
  335. return 0.0
  336. # Solar constant (W/m²)
  337. solar_constant = 1367.0
  338. # Day of year for seasonal correction
  339. n = date_time.timetuple().tm_yday
  340. # Earth-Sun distance correction factor
  341. # Accounts for elliptical orbit
  342. B = 2 * math.pi * (n - 1) / 365
  343. distance_factor = 1.000110 + 0.034221 * math.cos(B) + 0.001280 * math.sin(B) + \
  344. 0.000719 * math.cos(2*B) + 0.000077 * math.sin(2*B)
  345. # Extraterrestrial radiation on horizontal surface
  346. elevation_rad = math.radians(elevation)
  347. I0 = solar_constant * distance_factor * math.sin(elevation_rad)
  348. # Simplified atmospheric transmittance model
  349. # Air mass (Kasten and Young formula)
  350. air_mass = 1.0 / (math.sin(elevation_rad) + 0.50572 * (elevation + 6.07995)**(-1.6364))
  351. # Simplified atmospheric transmittance (assumes standard atmosphere)
  352. # Typical clear-sky conditions: ~0.7-0.8 transmittance
  353. # Components: Rayleigh scattering, aerosol extinction, water vapor, ozone
  354. tau_rayleigh = math.exp(-0.0903 * air_mass**0.84)
  355. tau_aerosol = math.exp(-0.0127 * air_mass**0.26) # Clean atmosphere assumption
  356. tau_water = math.exp(-0.0365 * air_mass**0.5) # Standard water vapor
  357. # Total transmittance
  358. transmittance = tau_rayleigh * tau_aerosol * tau_water
  359. # Clear-sky direct + diffuse radiation
  360. # Direct beam
  361. direct_beam = I0 * transmittance
  362. # Diffuse radiation (simplified - roughly 10-20% of direct for clear sky)
  363. diffuse_factor = 0.15
  364. diffuse = I0 * diffuse_factor
  365. # Total clear-sky radiation
  366. clear_sky_ghi = direct_beam + diffuse
  367. # Ensure non-negative
  368. return max(0.0, clear_sky_ghi)
  369. if __name__ == "__main__":
  370. # Test wind speed/direction calculations
  371. # u = 1.3415451049804688 # m/s eastward
  372. # v = -2.3990478515625 # m/s northward
  373. # speed, direction = calculate_wind_speed_direction(u, v)
  374. # print(f"Wind speed: {speed:.2f} m/s")
  375. # print(f"Wind direction: {direction:.2f}°")
  376. # Test nearest_position function
  377. # print("\n=== Testing Nearest Position Function ===")
  378. # # Create a sample grid
  379. # lats = np.linspace(45.0, 46.0, 10) # 10 points from 45°N to 46°N
  380. # lons = np.linspace(84.0, 85.0, 10) # 10 points from 84°E to 85°E
  381. # lat2d, lon2d = np.meshgrid(lats, lons)
  382. # # Test point (close to the target location in the notebook)
  383. # test_lat = 45.94375072
  384. # test_lon = 84.22134241
  385. # # Find nearest point
  386. # row, col = nearest_position(test_lat, test_lon, lat2d, lon2d)
  387. # print(f"Test point: ({test_lat}, {test_lon})")
  388. # print(f"Nearest grid point: ({lat2d[row, col]:.6f}, {lon2d[row, col]:.6f})")
  389. # print(f"Grid indices: row={row}, col={col}")
  390. # Test interpolation function with varying time frequencies
  391. print("\n=== Testing Weather Data Interpolation ===")
  392. # Create sample data with varying frequencies
  393. # First 6 hours: hourly data
  394. time1 = pd.date_range('2023-01-01 00:00', periods=7, freq='1h')
  395. temp1 = np.linspace(15, 18, 7) # Temperature rising from 15 to 18°C
  396. ws1 = np.linspace(5, 8, 7) # Wind speed increasing from 5 to 8 m/s
  397. # Next 6 hours: 3-hourly data
  398. time2 = pd.date_range('2023-01-01 06:00', periods=3, freq='3h')
  399. temp2 = np.linspace(18, 22, 3) # Temperature rising from 18 to 22°C
  400. ws2 = np.linspace(8, 12, 3) # Wind speed increasing from 8 to 12 m/s
  401. # Last 6 hours: 6-hourly data
  402. time3 = pd.date_range('2023-01-01 12:00', periods=2, freq='6h')
  403. temp3 = np.linspace(22, 20, 2) # Temperature dropping from 22 to 20°C
  404. ws3 = np.linspace(12, 10, 2) # Wind speed decreasing from 12 to 10 m/s
  405. # Combine all data
  406. times = np.concatenate([time1, time2[1:], time3[1:]]) # Remove overlapping points
  407. temperatures = np.concatenate([temp1, temp2[1:], temp3[1:]])
  408. wind_speeds = np.concatenate([ws1, ws2[1:], ws3[1:]])
  409. # Create DataFrame
  410. df_varying = pd.DataFrame({
  411. 'time': times,
  412. 'temperature': temperatures,
  413. 'wind_speed': wind_speeds,
  414. })
  415. print("Original data with varying frequencies:")
  416. print(df_varying)
  417. print(f"\nOriginal data shape: {df_varying.shape}")
  418. # Interpolate to 30-minute intervals
  419. df_interpolated = interpolate_weather_attributes(df_varying, 'time', '15min', method='linear')
  420. print(f"\nInterpolated data (30-min intervals):")
  421. print(df_interpolated.head(100))
  422. print(f"Interpolated data shape: {df_interpolated.shape}")
  423. print(f"\nTime range: {df_interpolated['time'].min()} to {df_interpolated['time'].max()}")
  424. print(f"Number of time points: {len(df_interpolated)}")
  425. # # u, v = calculate_wind_components(speed, direction)
  426. # # print(f"u component: {u:.2f} m/s")
  427. # # print(f"v component: {v:.2f} m/s")