"""Collection of classes and functions to obtain spectral parameters."""
import pvlib
import pandas as pd
import inspect
import warnings
from pvdeg import decorators
[docs]
@decorators.geospatial_quick_shape(
"timeseries",
[
"apparent_zenith",
"zenith",
"apparent_elevation",
"elevation",
"azimuth",
"equation_of_time",
],
)
def solar_position(weather_df: pd.DataFrame, meta: dict) -> pd.DataFrame:
"""Calculate solar position.
Calculate solar position using pvlib based on weather data from the National
Solar Radiation Database (NSRDB) for a given location (gid).
Parameters
----------
weather_df : pandas.DataFrame
Weather data for given location.
meta : dict
Meta data of location.
Returns
-------
solar_position : pandas.DataFrame
Solar position like zenith and azimuth.
"""
# location = pvlib.location.Location(
# latitude=meta['latitude'],
# longitude=meta['longitude'],
# altitude=meta['elevation'])
# TODO: check if timeshift is necessary
# times = weather_df.index
# solar_position = location.get_solarposition(times)
solar_position = pvlib.solarposition.get_solarposition(
time=weather_df.index,
latitude=meta["latitude"],
longitude=meta["longitude"],
altitude=meta["altitude"],
)
return solar_position
[docs]
@decorators.geospatial_quick_shape(
"timeseries",
[
"poa_global",
"poa_direct",
"poa_diffuse",
"poa_sky_diffuse",
"poa_ground_diffuse",
],
)
def poa_irradiance(
weather_df: pd.DataFrame,
meta: dict,
module_mount="fixed",
sol_position=None,
dni_extra=None,
airmass=None,
albedo=0.25,
surface_type=None,
sky_model="isotropic",
model_perez="allsitescomposite1990",
**kwargs_mount,
) -> pd.DataFrame:
"""Calculate plane-of-array (POA) irradiance.
Calculate plane-of-array (POA) irradiance using
`pvlib.irradiance.get_total_irradiance` for different module mounts
as fixed tilt systems or tracked systems.
Parameters
----------
weather_df : pd.DataFrame
The file path to the NSRDB file.
meta : dict
The geographical location ID in the NSRDB file.
module_mount: string
Module mounting configuration. Can either be `fixed` for fixed tilt systems or
`single_axis` for single-axis tracker systems.
sol_position : pd.DataFrame, optional
pvlib.solarposition.get_solarposition Dataframe. If none is given, it will be
calculated.
dni_extra : pd.Series, optional
Extra-terrestrial direct normal irradiance. If None, it will be calculated.
airmass : pd.Series, optional
Airmass values. If None, it will be calculated.
albedo : float, optional
Ground reflectance. Default is 0.25.
surface_type : str, optional
Type of surface for albedo calculation. If None, a default value will be used.
sky_model : str, optional
Sky diffuse model to use. Default is `isotropic`.
Options: 'isotropic', 'klucher', 'haydavies', 'reindl', 'king', 'perez'.
model_perez : str, optional
Perez model to use for diffuse irradiance. Default is `allsitescomposite1990`.
Only used when sky_model is 'perez'.
**kwargs_mount : dict
Additional keyword arguments for the POA model based on mounting configuration.
See `poa_irradiance_fixed` or `poa_irradiance_tracker` for details.
- For `module_mount='fixed'`:
- surface_tilt : float
Tilt angle of the modules (degrees).
- surface_azimuth : float
Azimuth angle of the modules (degrees).
- For `module_mount='single_axis'`:
- axis_tilt : float
Tilt angle of the tracker axis (degrees).
- axis_azimuth : float
Azimuth angle of the tracker axis (degrees).
- max_angle : float
Maximum rotation angle of the tracker (degrees).
- backtrack : bool
Whether to enable backtracking for single-axis trackers.
- gcr : float
Ground coverage ratio of the tracker system.
Returns
-------
poa : pandas.DataFrame
Contains keys/columns 'poa_global', 'poa_direct', 'poa_diffuse',
'poa_sky_diffuse', 'poa_ground_diffuse'. [W/m2]
Notes
-----
This function uses pvlib to calculate the plane-of-array irradiance based on the
provided weather data and mounting configuration. See the pvlib documentation for
further information on the parameters.
"""
# Allow legacy keys for tilt and azimuth
if "tilt" in kwargs_mount:
kwargs_mount["surface_tilt"] = kwargs_mount.pop("tilt")
if "azimuth" in kwargs_mount:
kwargs_mount["surface_azimuth"] = kwargs_mount.pop("azimuth")
def check_keys_in_list(dictionary, key_list):
extra_keys = [key for key in dictionary.keys() if key not in key_list]
for key in extra_keys:
dictionary.pop(key)
warnings.warn(
"Key '{extra_keys}' cannot be used for the selected `module_mount` "
"and are ignored."
)
if sol_position is None:
sol_position = solar_position(weather_df, meta)
if module_mount == "fixed":
arg_names = ["surface_tilt", "surface_azimuth"]
check_keys_in_list(kwargs_mount, arg_names)
poa = poa_irradiance_fixed(
weather_df=weather_df,
meta=meta,
sol_position=sol_position,
dni_extra=dni_extra,
airmass=airmass,
albedo=albedo,
surface_type=surface_type,
model=sky_model,
model_perez=model_perez,
**kwargs_mount,
)
elif module_mount == "single_axis":
args = inspect.signature(pvlib.tracking.singleaxis).parameters
arg_names = [param.name for param in args.values()]
check_keys_in_list(kwargs_mount, arg_names)
poa = poa_irradiance_tracker(
weather_df=weather_df,
meta=meta,
sol_position=sol_position,
dni_extra=dni_extra,
airmass=airmass,
albedo=albedo,
surface_type=surface_type,
model=sky_model,
model_perez=model_perez,
**kwargs_mount,
)
else:
raise NotImplementedError(
f"The input module_mount '{module_mount}' is not implemented"
)
return poa.fillna(0) # Fill NaN values with 0 for irradiance values
[docs]
@decorators.geospatial_quick_shape(
1,
[
"poa_global",
"poa_direct",
"poa_diffuse",
"poa_sky_diffuse",
"poa_ground_diffuse",
],
)
def poa_irradiance_fixed(
weather_df: pd.DataFrame,
meta: dict,
sol_position=None,
surface_tilt=None,
surface_azimuth=None,
dni_extra=None,
airmass=None,
albedo=0.25,
surface_type=None,
model="isotropic",
model_perez="allsitescomposite1990",
) -> pd.DataFrame:
"""
Calculate plane-of-array (POA) irradiance using
`pvlib.irradiance.get_total_irradiance` for a fixed tilt system.
Parameters
----------
weather_df : pd.DataFrame
The file path to the NSRDB file.
meta : dict
The geographical location ID in the NSRDB file.
sol_position : pd.DataFrame, optional
pvlib.solarposition.get_solarposition Dataframe. If none is given, it will be
calculated.
dni_extra : pd.Series, optional
Extra-terrestrial direct normal irradiance. If None, it will be calculated.
airmass : pd.Series, optional
Airmass values. If None, it will be calculated.
albedo : float, optional
Ground reflectance. Default is 0.25.
surface_type : str, optional
Type of surface for albedo calculation. If None, a default value will be used.
sky_model : str, optional
Sky diffuse model to use. Default is `isotropic`.
Options: 'isotropic', 'klucher', 'haydavies', 'reindl', 'king', 'perez'.
model_perez : str, optional
Perez model to use for diffuse irradiance. Default is `allsitescomposite1990`.
Only used when sky_model is 'perez'.
surface_tilt : float, optional
Tilt angle of the modules (degrees).
surface_azimuth : float, optional
Azimuth angle of the modules (degrees).
Returns
-------
poa : pd.DataFrame
DataFrame containing the calculated plane-of-array irradiance components with
columns:
'poa_global', 'poa_direct', 'poa_diffuse', 'poa_sky_diffuse',
'poa_ground_diffuse'. [W/m2]
Notes
-----
This function uses pvlib to calculate the plane-of-array irradiance based on the
provided weather data and mounting configuration for fixed tilt systems. See the
pvlib documentation for further information on the parameters.
"""
if surface_tilt is None:
try:
surface_tilt = float(meta["tilt"])
except Exception:
surface_tilt = float(abs(meta["latitude"]))
print(
"The array surface_tilt angle was not provided, therefore the latitude "
f"of {surface_tilt: .1f} was used."
)
if surface_azimuth is None: # Sets the default orientation to equator facing.
try:
surface_azimuth = float(meta["azimuth"])
except Exception:
if float(meta["latitude"]) < 0:
surface_azimuth = 0
else:
surface_azimuth = 180
print(
"The array azimuth was not provided, therefore an azimuth of "
f"{surface_azimuth: .1f} was used."
)
if sol_position is None:
sol_position = solar_position(weather_df, meta)
poa = pvlib.irradiance.get_total_irradiance(
surface_tilt=surface_tilt,
surface_azimuth=surface_azimuth,
dni=weather_df["dni"],
ghi=weather_df["ghi"],
dhi=weather_df["dhi"],
solar_zenith=sol_position["apparent_zenith"],
solar_azimuth=sol_position["azimuth"],
dni_extra=dni_extra,
airmass=airmass,
albedo=albedo,
surface_type=surface_type,
model=model,
model_perez=model_perez,
)
return poa
[docs]
@decorators.geospatial_quick_shape(
1,
[
"poa_global",
"poa_direct",
"poa_diffuse",
"poa_sky_diffuse",
"poa_ground_diffuse",
],
)
def poa_irradiance_tracker(
weather_df: pd.DataFrame,
meta: dict,
sol_position=None,
axis_tilt=0,
axis_azimuth=None,
max_angle=90,
backtrack=True,
gcr=0.2857142857142857,
cross_axis_tilt=0,
dni_extra=None,
airmass=None,
albedo=0.25,
surface_type=None,
model="isotropic",
model_perez="allsitescomposite1990",
) -> pd.DataFrame:
"""
Calculate plane-of-array (POA) irradiance using pvlib based on supplied weather
data.
Parameters
----------
weather_df : pd.DataFrame
The weather data.
meta : dict
The geographical location information.
sol_position : pd.DataFrame, optional
pvlib.solarposition.get_solarposition Dataframe. If none is given, it will be
calculated.
axis_tilt : float, optional
The tilt angle of the array along the long axis of a single axis tracker
[degrees].
If None, horizontal is used.
axis_azimuth : float, optional
The azimuth angle of the long, non-rotating axis of the array panels [degrees].
North-south orientation by default.
max_angle : float
This is the maximum angle of the rotating axis achievable relative to the
horizon.
backtrack : boolean
If true, the tilt will backtrack to avoid row to row shading.
gcr : float
Ground coverage ratio (GCR). The ratio of the width of the PV array to the
distance between rows. This affects the backtracking function.
cross_axis_tilt : float
angle, relative to horizontal, of the line formed by the intersection between
the slope containing the tracker axes and a plane perpendicular to the tracker
axes [degrees]
Fixes backtracking for a slope not parallel with the axis azimuth.
dni_extra : pd.Series, optional
Extra-terrestrial direct normal irradiance. If None, it will be calculated.
airmass : pd.Series, optional
Airmass values. If None, it will be calculated.
albedo : float, optional
Ground reflectance. Default is 0.25.
surface_type : str, optional
Type of surface for albedo calculation. If None, a default value will be used.
sky_model : str, optional
Sky diffuse model to use. Default is `isotropic`.
Options: 'isotropic', 'klucher', 'haydavies', 'reindl', 'king', 'perez'.
model_perez : str, optional
Perez model to use for diffuse irradiance. Default is `allsitescomposite1990`.
Only used when sky_model is 'perez'.
axis_tilt : float, optional
Tilt angle of the tracker axis (degrees). Default is 0.0.
axis_azimuth : float, optional
Azimuth angle of the tracker axis (degrees). Default is 0.0.
max_angle : float, optional
Maximum rotation angle of the tracker (degrees). Default is 45.0.
backtrack : bool, optional
Whether to enable backtracking for single-axis trackers. Default is True.
gcr : float, optional
Ground coverage ratio of the tracker system. Default is 0.3.
Returns
-------
tracker_poa : pandas.DataFrame
Contains keys/columns 'poa_global', 'poa_direct', 'poa_diffuse',
'poa_sky_diffuse', 'poa_ground_diffuse'. [W/m2]
Notes
-----
This function uses pvlib to calculate the plane-of-array irradiance based on the
provided weather data and mounting configuration for single-axis tracker systems.
See the pvlib documentation for further information on the parameters.
"""
if axis_azimuth is None: # Sets the default orientation to north-south.
try:
axis_azimuth = float(meta["axis_azimuth"])
except Exception:
if float(meta["latitude"]) < 0:
axis_azimuth = 0
else:
axis_azimuth = 180
print(
"The array axis_azimuth was not provided, therefore an azimuth of "
f"{axis_azimuth: .1f} was used."
)
if axis_tilt is None: # Sets the default orientation to horizontal.
try:
axis_tilt = float(meta["axis_tilt"])
except Exception:
axis_tilt = 0
print(
"The array axis_tilt was not provided, therefore an axis tilt of 0° "
"was used."
)
if sol_position is None:
sol_position = solar_position(weather_df, meta)
tracker_data = pvlib.tracking.singleaxis(
sol_position["apparent_zenith"],
sol_position["azimuth"],
axis_tilt=axis_tilt,
axis_azimuth=axis_azimuth,
max_angle=max_angle,
backtrack=backtrack,
gcr=gcr,
cross_axis_tilt=cross_axis_tilt,
)
tracker_poa = pvlib.irradiance.get_total_irradiance(
surface_tilt=tracker_data["surface_tilt"],
surface_azimuth=tracker_data["surface_azimuth"],
dni=weather_df["dni"],
ghi=weather_df["ghi"],
dhi=weather_df["dhi"],
solar_zenith=sol_position["apparent_zenith"],
solar_azimuth=sol_position["azimuth"],
dni_extra=dni_extra,
airmass=airmass,
albedo=albedo,
surface_type=surface_type,
model=model,
model_perez=model_perez,
)
return tracker_poa