Coverage for aixweather/transformation_functions/variable_transformations.py: 92%

77 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-01-06 16:01 +0000

1""" 

2This module includes variable transformations and functions 

3to calculate variables from given values 

4""" 

5import logging 

6 

7import pandas as pd 

8import numpy as np 

9 

10import pvlib 

11 

12from aixweather.imports.utils_import import MetaData 

13 

14 

15logger = logging.getLogger(__name__) 

16 

17 

18def approximate_opaque_from_total_skycover(total_sky_cover): 

19 """ 

20 Approximate opaque sky cover from total sky cover by assuming they are equal. 

21 

22 This is a simple approximation that assumes all clouds in the total sky cover are opaque. 

23 In reality, some clouds may be translucent, making this an upper bound estimate. 

24 

25 Args: 

26 total_sky_cover (float): Total sky cover in oktas or percentage of sky covered by clouds. 

27 

28 Returns: 

29 float: Approximated opaque sky cover in the same units as the input. 

30 """ 

31 opaque_sky_cover = total_sky_cover 

32 return opaque_sky_cover 

33 

34 

35def calculate_dew_point_temp(dry_bulb_temp: pd.Series, rel_hum: pd.Series) -> pd.Series: 

36 """ 

37 Calculate the dew point temperature using a simplified formula. 

38 

39 This function estimates the dew point temperature in degrees Celsius based on the given dry bulb temperature 

40 and relative humidity. Please note that this formula is fairly accurate for humidity above 50%. For more 

41 precise calculations, consider using a more advanced formula. 

42 

43 Args: 

44 dry_bulb_temp (pd.Series): The dry bulb temperature in degrees Celsius. 

45 rel_hum (pd.Series): The relative humidity in percentage. 

46 

47 Returns: 

48 pd.Series: The estimated dew point temperature in degrees Celsius. 

49 """ 

50 dew_point_temp = dry_bulb_temp - (100 - rel_hum) / 5 

51 return dew_point_temp 

52 

53 

54def calculate_direct_horizontal_radiation( 

55 glob_hor_rad: pd.Series, diff_hor_rad: pd.Series 

56) -> pd.Series: 

57 """ 

58 Calculate direct horizontal radiation by subtracting diffuse horizontal radiation from global horizontal radiation. 

59 

60 Checked by Martin Rätz on 08.08.2023. 

61 

62 Args: 

63 glob_hor_rad (pd.Series): Global horizontal radiation in Wh/m^2. 

64 diff_hor_rad (pd.Series): Diffuse horizontal radiation in Wh/m^2. 

65 

66 Returns: 

67 pd.Series: The estimated direct horizontal radiation in Wh/m^2. 

68 """ 

69 

70 dir_hor_rad = glob_hor_rad - diff_hor_rad 

71 if isinstance(dir_hor_rad, pd.Series): 

72 dir_hor_rad.clip(0, inplace=True) 

73 else: 

74 if dir_hor_rad < 0: 

75 dir_hor_rad = 0 

76 return dir_hor_rad 

77 

78 

79def calculate_global_horizontal_radiation( 

80 dir_hor_rad: pd.Series, diff_hor_rad: pd.Series 

81) -> pd.Series: 

82 """ 

83 Calculate global horizontal radiation on a horizontal plane by adding direct horizontal radiation 

84 with diffuse horizontal radiation. 

85 

86 Checked by Martin Rätz on 08.08.2023 using: 

87 https://pvpmc.sandia.gov/modeling-steps/1-weather-design-inputs/irradiance-insolation/global-horizontal-irradiance/ 

88 

89 Args: 

90 dir_hor_rad (pd.Series): Direct horizontal radiation in Wh/m^2. 

91 diff_hor_rad (pd.Series): Diffuse horizontal radiation in Wh/m^2. 

92 

93 Returns: 

94 pd.Series: The estimated global horizontal radiation in Wh/m^2. 

95 """ 

96 

97 glob_hor_rad = dir_hor_rad + diff_hor_rad 

98 

99 # if value < 0 set as 0 

100 if isinstance(glob_hor_rad, pd.Series): 

101 glob_hor_rad.clip(0, inplace=True) 

102 else: 

103 if glob_hor_rad < 0: 

104 glob_hor_rad = 0 

105 

106 return glob_hor_rad 

107 

108 

109def calculate_horizontal_infrared_radiation( 

110 dry_bulb_temp: pd.Series, dew_point_temp: pd.Series, opaque_sky_cover: pd.Series 

111) -> pd.Series: 

112 """ 

113 Calculate horizontal infrared radiation using the provided inputs. 

114 

115 Calculation details can be found at: 

116 https://www.energyplus.net/sites/default/files/docs/site_v8.3.0/EngineeringReference/05-Climate/index.html 

117 

118 Sky emissivity is considered in the calculation as per the above reference. 

119 

120 Args: 

121 dry_bulb_temp (pd.Series): Dry bulb temperature in degrees Celsius. 

122 dew_point_temp (pd.Series): Dew point temperature in degrees Celsius. 

123 opaque_sky_cover (pd.Series): Opaque sky cover in tenths. 

124 

125 Returns: 

126 pd.Series: The estimated horizontal infrared radiation in Wh/m^2. 

127 """ 

128 

129 # change to Kelvin 

130 dry_bulb_temp = dry_bulb_temp + 273.15 

131 dew_point_tem = dew_point_temp + 273.15 

132 

133 sky_emissivity = (0.787 + 0.764 * np.log(dew_point_tem / 273.0)) * ( 

134 1.0 

135 + 0.0224 * opaque_sky_cover 

136 - 0.0035 * np.power(opaque_sky_cover, 2) 

137 + 0.00028 * np.power(opaque_sky_cover, 3) 

138 ) 

139 """equation 3 from Sky Radiation Modeling 

140 https://www.energyplus.net/sites/default/files/docs/site_v8.3.0/ 

141 EngineeringReference/05-Climate/index.html""" 

142 

143 hor_infra = ( 

144 sky_emissivity * 5.6697 * np.power(10.0, -8.0) * np.power(dry_bulb_temp, 4) 

145 ) 

146 """equation 2 from Sky Radiation Modeling  

147 https://www.energyplus.net/sites/default/files/docs/site_v8.3.0/EngineeringReference/05-Climate/index.html 

148 """ 

149 return hor_infra 

150 

151 

152def calculate_normal_from_horizontal_direct_radiation( 

153 latitude: float, 

154 longitude: float, 

155 utc_time: pd.DatetimeIndex, 

156 direct_horizontal_radiation: pd.Series, 

157): 

158 """ 

159 Calculates the Direct Normal Irradiance (DNI) from Direct Horizontal Irradiance (DHI). 

160 Values will be set to zero when the sun is below 5° angle from the horizon. 

161 

162 Args: 

163 latitude (float): The latitude of the location in degrees. 

164 longitude (float): The longitude of the location in degrees. 

165 utc_time (pd.DatetimeIndex): The timestamps for which the DNI is to be calculated, in UTC. 

166 direct_horizontal_radiation (pd.Series): The Direct Horizontal Irradiance (DHI) in [W/m^2] 

167 

168 Returns: 

169 pd.Series: The calculated Direct Normal Irradiance (DNI) in W/m^2 for each timestamp. 

170 """ 

171 

172 # Calculate the solar zenith angle for each time 

173 solar_position = pvlib.solarposition.get_solarposition( 

174 utc_time.tz_localize("UTC"), latitude, longitude 

175 ) 

176 solar_position.index = solar_position.index.tz_localize(None) 

177 zenith_angle_deg = solar_position["zenith"] 

178 

179 # Calculate the Direct Normal Irradiance (DNI) for each time 

180 zenith_angle_rad = zenith_angle_deg * (np.pi / 180) # Convert to radians 

181 dni = direct_horizontal_radiation / np.cos(zenith_angle_rad) 

182 

183 # Set values to zero when the sun is below 5° angle from the horizon 

184 dni = np.where(zenith_angle_deg > 85, 0, dni) 

185 

186 return dni 

187 

188 

189# wrapping functions ------------------------------------------ 

190def robust_transformation( 

191 df: pd.DataFrame, desired_variable: str, transformation_function, *args: str 

192) -> tuple[pd.DataFrame, dict]: 

193 """ 

194 Apply a transformation function to calculate the desired variable in the DataFrame 

195 if the variable is missing or contains only NaN values. Skip the transformation if 

196 any required column (specified in args) is missing to perform the calculation. 

197 

198 Args: 

199 df (pd.DataFrame): DataFrame containing the data. 

200 desired_variable (str): Name of the variable to which the transformation should be applied. 

201 transformation_function: Function to apply to the desired variable. 

202 *args: Additional arguments required for the transformation function. 

203 These can be column names (str) or other values (all other types). 

204 

205 Returns: 

206 tuple[pd.DataFrame, dict]: A tuple containing the updated DataFrame with the transformation applied 

207 to the desired variable and a dictionary indicating which variables were used for the calculation. 

208 """ 

209 

210 # Feed back whether calculation was done and with which variables 

211 calc_status = {} 

212 

213 # Check if the desired variable is missing or contains only NaN values 

214 if desired_variable not in df.columns or df[desired_variable].isna().all(): 

215 # refactor args to account for strings to get the column of the df and pure objects 

216 new_args = list() 

217 args_of_columns = list() # get only strings of args that are column names 

218 for arg in args: 

219 # if arg is a column of the df, the new arg should be the column values 

220 if isinstance(arg, str): 

221 if arg in df.columns: 

222 # if contains only NaN values 

223 if df[arg].isna().all(): 

224 logger.debug( 

225 "The required variable %s has only nan, " 

226 "calculation of %s aborted.", 

227 arg, desired_variable 

228 ) 

229 return df, calc_status 

230 new_args.append(df[arg]) 

231 args_of_columns.append(arg) 

232 else: 

233 logger.debug( 

234 "The required variable %s is not in the dataframes " 

235 "columns, calculation of %s aborted.", 

236 arg, desired_variable 

237 ) 

238 return df, calc_status 

239 else: 

240 # if arg is an object, e.g. from meta_data, add it directly 

241 new_args.append(arg) 

242 

243 # Apply transformation if variables are available 

244 df[desired_variable] = transformation_function(*new_args) 

245 logger.info("Calculated %s from %s.", desired_variable, args_of_columns) 

246 

247 # Feed back whether calculation was done and with which variables 

248 calc_status = {desired_variable: args_of_columns} 

249 

250 return df, calc_status 

251 

252 

253def variable_transform_all( 

254 df: pd.DataFrame, meta: MetaData 

255) -> tuple[pd.DataFrame, dict]: 

256 """ 

257 Transform and compute missing variables for the given DataFrame based on specified calculations. 

258 This function performs multiple transformations on the DataFrame to calculate all completely 

259 missing variables. The applied calculations for each variable are tracked in the 

260 'calc_overview' dictionary. 

261 

262 You can add all variable transformations here. 

263 Respect a meaningful order. 

264 

265 Args: 

266 df (pd.DataFrame): The DataFrame containing the data to be transformed. 

267 meta (MetaData): Metadata associated with the data. 

268 

269 Returns: 

270 tuple[pd.DataFrame, dict]: A tuple containing the transformed DataFrame with computed variables 

271 and a dictionary ('calc_overview') tracking the calculations applied to the DataFrame. 

272 """ 

273 

274 # track if calculations are applied 

275 calc_overview = {} 

276 

277 ### calculate missing variables for core data format if required and if possible 

278 df, calc_status = robust_transformation( 

279 df, "OpaqueSkyCover", approximate_opaque_from_total_skycover, "TotalSkyCover" 

280 ) 

281 calc_overview.update(calc_status) 

282 

283 df, calc_status = robust_transformation( 

284 df, "DewPointTemp", calculate_dew_point_temp, "DryBulbTemp", "RelHum" 

285 ) 

286 calc_overview.update(calc_status) 

287 

288 df, calc_status = robust_transformation( 

289 df, 

290 "DirHorRad", 

291 calculate_direct_horizontal_radiation, 

292 "GlobHorRad", 

293 "DiffHorRad", 

294 ) 

295 calc_overview.update(calc_status) 

296 

297 df, calc_status = robust_transformation( 

298 df, 

299 "GlobHorRad", 

300 calculate_global_horizontal_radiation, 

301 "DirHorRad", 

302 "DiffHorRad", 

303 ) 

304 calc_overview.update(calc_status) 

305 

306 df, calc_status = robust_transformation( 

307 df, 

308 "DirNormRad", 

309 calculate_normal_from_horizontal_direct_radiation, 

310 meta.latitude, 

311 meta.longitude, 

312 df.index, 

313 "DirHorRad", 

314 ) 

315 calc_overview.update(calc_status) 

316 

317 df, calc_status = robust_transformation( 

318 df, 

319 "HorInfra", 

320 calculate_horizontal_infrared_radiation, 

321 "DryBulbTemp", 

322 "DewPointTemp", 

323 "OpaqueSkyCover", 

324 ) 

325 calc_overview.update(calc_status) 

326 

327 

328 return df, calc_overview