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
« 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
7import pandas as pd
8import numpy as np
10import pvlib
12from aixweather.imports.utils_import import MetaData
15logger = logging.getLogger(__name__)
18def approximate_opaque_from_total_skycover(total_sky_cover):
19 """
20 Approximate opaque sky cover from total sky cover by assuming they are equal.
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.
25 Args:
26 total_sky_cover (float): Total sky cover in oktas or percentage of sky covered by clouds.
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
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.
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.
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.
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
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.
60 Checked by Martin Rätz on 08.08.2023.
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.
66 Returns:
67 pd.Series: The estimated direct horizontal radiation in Wh/m^2.
68 """
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
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.
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/
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.
93 Returns:
94 pd.Series: The estimated global horizontal radiation in Wh/m^2.
95 """
97 glob_hor_rad = dir_hor_rad + diff_hor_rad
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
106 return glob_hor_rad
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.
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
118 Sky emissivity is considered in the calculation as per the above reference.
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.
125 Returns:
126 pd.Series: The estimated horizontal infrared radiation in Wh/m^2.
127 """
129 # change to Kelvin
130 dry_bulb_temp = dry_bulb_temp + 273.15
131 dew_point_tem = dew_point_temp + 273.15
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"""
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
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.
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]
168 Returns:
169 pd.Series: The calculated Direct Normal Irradiance (DNI) in W/m^2 for each timestamp.
170 """
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"]
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)
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)
186 return dni
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.
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).
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 """
210 # Feed back whether calculation was done and with which variables
211 calc_status = {}
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)
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)
247 # Feed back whether calculation was done and with which variables
248 calc_status = {desired_variable: args_of_columns}
250 return df, calc_status
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.
262 You can add all variable transformations here.
263 Respect a meaningful order.
265 Args:
266 df (pd.DataFrame): The DataFrame containing the data to be transformed.
267 meta (MetaData): Metadata associated with the data.
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 """
274 # track if calculations are applied
275 calc_overview = {}
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)
283 df, calc_status = robust_transformation(
284 df, "DewPointTemp", calculate_dew_point_temp, "DryBulbTemp", "RelHum"
285 )
286 calc_overview.update(calc_status)
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)
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)
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)
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)
328 return df, calc_overview