"""Collection of classes and functions to calculate different temperatures."""
import pvlib
# import pvdeg
from pvdeg import (
spectral,
decorators,
)
import pandas as pd
from typing import Union
import inspect
[docs]
def map_model(temp_model: str, cell_or_mod: str) -> callable:
"""Map string to pvlib function.
References
----------
https://pvlib-python.readthedocs.io/en/stable/reference/pv_modeling/temperature.html
"""
# sapm_cell_from_module?
# prillman diverges from others, used for smoothing/interp
# double check that models are in correct maps
module = { # only module
"sapm": pvlib.temperature.sapm_module,
"sapm_mod": pvlib.temperature.sapm_module,
}
cell = { # only cell
"sapm": pvlib.temperature.sapm_cell,
"sapm_cell": pvlib.temperature.sapm_cell,
"pvsyst": pvlib.temperature.pvsyst_cell,
"ross": pvlib.temperature.ross,
"noct_sam": pvlib.temperature.noct_sam,
"generic_linear": pvlib.temperature.generic_linear,
}
agnostic = { # module or cell
"faiman": pvlib.temperature.faiman,
"faiman_rad": pvlib.temperature.faiman_rad,
"fuentes": pvlib.temperature.fuentes,
}
super_map = {"module": module, "cell": cell}
if cell_or_mod:
agnostic.update(super_map[cell_or_mod]) # bad naming
else:
agnostic.update(module) # if none then we use all
agnostic.update(cell)
return agnostic[temp_model]
def _wind_speed_factor(temp_model: str, meta: dict, wind_factor: float):
if temp_model == "sapm":
wind_speed_factor = (10 / float(meta["wind_height"])) ** wind_factor
elif temp_model == "pvsyst":
wind_speed_factor = (2 / float(meta["wind_height"])) ** wind_factor
elif temp_model == "faiman":
wind_speed_factor = (2 / float(meta["wind_height"])) ** wind_factor
elif temp_model == "faiman_rad":
wind_speed_factor = (2 / float(meta["wind_height"])) ** wind_factor
elif temp_model == "fuentes":
wind_speed_factor = (5 / float(meta["wind_height"])) ** wind_factor
elif temp_model == "ross":
wind_speed_factor = (
10 / float(meta["wind_height"])
) ** wind_factor # I had to guess what this one was
elif temp_model == "noct_sam":
if meta["wind_height"] > 3:
wind_speed_factor = 2
else:
wind_speed_factor = (
1 # The wind speed height is managed weirdly for this one.
)
elif temp_model == "prilliman":
wind_speed_factor = (
1 # this model will take the wind speed height in and do an adjustment.
)
elif temp_model == "generic_linear":
wind_speed_factor = (10 / float(meta["wind_height"])) ** wind_factor
elif temp_model == "GenericLinearModel":
wind_speed_factor = (2 / float(meta["wind_height"])) ** wind_factor
# this one does a linear conversion from the other models, faiman, pvsyst,
# noct_sam, sapm_module and generic_linear.
# An appropriate facter will need to be figured out.
else:
wind_speed_factor = 1 # this is just hear for completeness.
return wind_speed_factor
[docs]
@decorators.geospatial_quick_shape("timeseries", ["module_temperature"])
def module(
weather_df,
meta,
poa=None,
temp_model="sapm",
conf="open_rack_glass_polymer",
wind_factor=0.33,
):
"""Calculate module surface temperature using pvlib.
Parameters
----------
weather_df : pd.dataframe
Data Frame with minimum requirements of 'temp_air' and 'wind_speed'
poa : pandas.DataFrame
Contains keys/columns 'poa_global', 'poa_direct', 'poa_diffuse',
'poa_sky_diffuse', 'poa_ground_diffuse'.
temp_model : str, optional
The temperature model to use, Sandia Array Performance Model 'sapm'
from pvlib by default.
conf : str, optional
The configuration of the PV module architecture and mounting
configuration.
Options:
'sapm': 'open_rack_glass_polymer' (default),
'open_rack_glass_glass', 'close_mount_glass_glass',
'insulated_back_glass_polymer'
Returns
-------
module_temperature : pandas.Series
The module temperature in degrees Celsius at each time step.
"""
parameters = pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS[temp_model][conf]
if poa is None:
poa = spectral.poa_irradiance(weather_df, meta)
if "wind_height" not in meta.keys():
wind_speed_factor = 1
else:
if temp_model == "sapm":
wind_speed_factor = (10 / float(meta["wind_height"])) ** wind_factor
elif temp_model == "pvsyst":
wind_speed_factor = (2 / float(meta["wind_height"])) ** wind_factor
elif temp_model == "faiman":
wind_speed_factor = (2 / float(meta["wind_height"])) ** wind_factor
elif temp_model == "faiman_rad":
wind_speed_factor = (2 / float(meta["wind_height"])) ** wind_factor
elif temp_model == "fuentes":
wind_speed_factor = (5 / float(meta["wind_height"])) ** wind_factor
elif temp_model == "ross":
wind_speed_factor = (
10 / float(meta["wind_height"])
) ** wind_factor # guessed the temperature model height on this one, Kempe
elif temp_model == "noct_sam":
if meta["wind_height"] > 3:
wind_speed_factor = 2
else:
wind_speed_factor = (
1 # The wind speed height is managed weirdly for this one.
)
elif temp_model == "prilliman":
wind_speed_factor = (
1 # this model will take the wind speed height in and do an adjustment.
)
elif temp_model == "generic_linear":
wind_speed_factor = (10 / float(meta["wind_height"])) ** wind_factor
elif temp_model == "GenericLinearModel":
wind_speed_factor = (2 / float(meta["wind_height"])) ** wind_factor
# this one does a linear conversion from the other models, faiman, pvsyst,
# noct_sam, sapm_module and generic_linear.
# An appropriate facter will need to be figured out.
else:
wind_speed_factor = 1 # this is just hear for completeness.
# TODO put in code for the other models, PVSYS, Faiman,
if temp_model == "sapm":
module_temperature = pvlib.temperature.sapm_module(
poa_global=poa["poa_global"],
temp_air=weather_df["temp_air"],
wind_speed=weather_df["wind_speed"] * wind_speed_factor,
a=parameters["a"],
b=parameters["b"],
)
else:
# TODO: add options for temperature model
print("There are other models but they haven't been implemented yet!")
return module_temperature
[docs]
@decorators.geospatial_quick_shape("timeseries", ["cell_temperature"])
def cell(
weather_df: pd.DataFrame,
meta: dict,
poa: Union[pd.DataFrame, pd.Series] = None,
temp_model: str = "sapm",
conf: str = "open_rack_glass_polymer",
wind_factor: float = 0.33,
) -> pd.DataFrame:
"""Calculate the PV cell temperature using pvlib-python.
Currently this only supports the SAPM temperature model.
Parameters
-----------
weather_df : (pd.dataframe)
Data Frame with minimum requirements of 'temp_air' and 'wind_speed'
meta : (dict)
Weather meta-data dictionary (location info)
poa : (dataframe or series, optional)
Dataframe or series with minimum requirement of 'poa_global'
temp_model : (str, optional)
Specify which temperature model from pvlib to use. Current options:
'sapm'
conf : (str)
The configuration of the PV module architecture and mounting
configuration.
Options: 'open_rack_glass_polymer' (default), 'open_rack_glass_glass',
'close_mount_glass_glass', 'insulated_back_glass_polymer'
wind_factor : float, optional
Wind speed correction exponent to account for different wind speed measurement
heights between weather database (e.g. NSRDB) and the tempeature model
(e.g. SAPM). The NSRDB provides calculations at 2 m (i.e module height) but SAPM
uses a 10m height.
It is recommended that a power-law relationship between height and wind speed
of 0.33 be used*. This results in a wind speed that is 1.7 times higher. It is
acknowledged that this can vary significantly.
R. Rabbani, M. Zeeshan, "Exploring the suitability of MERRA-2 reanalysis data for
wind energy estimation, analysis of wind characteristics and energy potential
assessment for selected sites in Pakistan", Renewable Energy 154 (2020) 1240-1251.
Return:
-------
temp_cell : pandas.Series
This is the temperature of the cell in a module at every time step.[°C]
"""
if "wind_height" not in meta.keys():
wind_speed_factor = 1
else:
if temp_model == "sapm":
wind_speed_factor = (10 / float(meta["wind_height"])) ** wind_factor
elif temp_model == "pvsyst":
wind_speed_factor = (2 / float(meta["wind_height"])) ** wind_factor
elif temp_model == "faiman":
wind_speed_factor = (2 / float(meta["wind_height"])) ** wind_factor
elif temp_model == "faiman_rad":
wind_speed_factor = (2 / float(meta["wind_height"])) ** wind_factor
elif temp_model == "fuentes":
wind_speed_factor = (5 / float(meta["wind_height"])) ** wind_factor
elif temp_model == "ross":
wind_speed_factor = (
10 / float(meta["wind_height"])
) ** wind_factor # I had to guess what the wind height for this temperature
# model was on this one, Kempe.
elif temp_model == "notc_sam":
if float(meta["wind_height"]) > 3:
wind_speed_factor = 2
else:
wind_speed_factor = (
1 # The wind speed height is managed weirdly for this one.
)
elif temp_model == "prilliman":
wind_speed_factor = (
1 # this model will take the wind speed height in and do an adjustment.
)
elif temp_model == "generic_linear":
wind_speed_factor = (10 / float(meta["wind_height"])) ** wind_factor
elif temp_model == "GenericLinearModel":
wind_speed_factor = (2 / float(meta["wind_height"])) ** wind_factor
# this one does a linear conversion from the other models, faiman, pvsyst,
# noct_sam, sapm_module and generic_linear.
# An appropriate facter will need to be figured out.
else:
wind_speed_factor = 1 # this is just here for completeness.
parameters = pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS[temp_model][conf]
if poa is None:
poa = spectral.poa_irradiance(weather_df, meta)
if temp_model == "sapm":
temp_cell = pvlib.temperature.sapm_cell(
poa_global=poa["poa_global"],
temp_air=weather_df["temp_air"],
wind_speed=weather_df["wind_speed"] * wind_speed_factor,
**parameters,
)
else:
# TODO: add options for temperature model
print("There are other models but they haven't been implemented yet!")
return temp_cell
# test not providing poa
# what if we dont need the cell or mod param, only matters for sapm
# to add more temperature model options just add them to the model_map function with the
# value as a reference to your function
# genaric linear is a little weird, we need to calculate the values outside of the
# function using pvlib.temp.genearilinearmodel and converting, can we take a reference
# to the model or args for the model instead
[docs]
def temperature(
weather_df,
meta,
poa=None,
temp_model="sapm",
cell_or_mod=None,
conf="open_rack_glass_polymer",
wind_factor=0.33,
irradiance_kwarg={},
model_kwarg={},
):
"""
Calculate the PV cell or module temperature using PVLIB.
Current supports the following temperature models:
Parameters
-----------
cell_or_mod : (str)
choose to calculate the cell or module temperature. Use
``cell_or_mod == 'mod' or 'module'`` for module temp calculation.
``cell_or_mod == 'cell'`` for cell temp calculation.
weather_df : (pd.dataframe)
Data Frame with minimum requirements of 'temp_air' and 'wind_speed'
meta : (dict)
Weather meta-data dictionary (location info)
poa : (dataframe or series, optional)
Dataframe or series with minimum requirement of 'poa_global'. Will be calculated
rom weather_df, meta if not provided
temp_model : (str, optional)
Specify which temperature model from pvlib to use. Current options:
``sapm_cell``,``sapm_module``,``pvsyst_cell``,``faiman``,``faiman_rad``,
``ross``,``noct_sam``, ``fuentes``, ``generic_linear``
conf : (str)
The configuration of the PV module architecture and mounting
configuration. Currently only used for 'sapm' and 'pvsys'.
With different options for each.
'sapm' options: ``open_rack_glass_polymer`` (default),
``open_rack_glass_glass``, ``close_mount_glass_glass``,
``insulated_back_glass_polymer``
'pvsys' options: ``freestanding``, ``insulated``
wind_factor : float, optional
Wind speed correction exponent to account for different wind speed measurement
heights between weather database (e.g. NSRDB) and the tempeature model
(e.g. SAPM). The NSRDB provides calculations at 2 m (i.e module height) but SAPM
uses a 10m height.
It is recommended that a power-law relationship between height and wind speed
of 0.33 be used*. This results in a wind speed that is 1.7 times higher. It is
acknowledged that this can vary significantly.
irradiance_kwarg : (dict, optional)
keyword argument dictionary used for the poa irradiance caluation.
options: ``sol_position``, ``tilt``, ``azimuth``, ``sky_model``. See
``pvdeg.spectral.poa_irradiance``.
model_kwarg : (dict, optional)
keyword argument dictionary used for the pvlib temperature model calculation.
See https://pvlib-python.readthedocs.io/en/stable/reference/pv_modeling/temperature.html # noqa
for more.
Return
-------
temp_cell : pandas.DataFrame
This is the temperature of the cell in a module at every time step.[°C]
References
-----------
R. Rabbani, M. Zeeshan, "Exploring the suitability of MERRA-2 reanalysis data for
wind energy estimation, analysis of wind characteristics and energy potential
assessment for selected sites in Pakistan", Renewable Energy 154 (2020) 1240-1251.
"""
cell_or_mod = "module" if cell_or_mod == "mod" else cell_or_mod # mod->module
if "wind_height" not in meta.keys():
wind_speed_factor = 1
else:
wind_speed_factor = _wind_speed_factor(temp_model, meta, wind_factor)
if temp_model in ["sapm", "pvsyst"]:
parameters = pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS[temp_model][conf]
if cell_or_mod != "cell" and temp_model == "sapm":
# strip the 'deltaT' for calculations that will not need it
parameters = {k: v for k, v in parameters.items() if k != "deltaT"}
if poa is None:
poa = spectral.poa_irradiance(weather_df, meta, **irradiance_kwarg)
# irrelevant key,value pair will be ignored (NO ERROR)
weather_args = {
"poa_global": poa["poa_global"],
"temp_air": weather_df["temp_air"],
"wind_speed": weather_df["wind_speed"] * wind_speed_factor,
} # but this will ovewrite the model default always, so will have to provide
# default wind speed in the kwargs
# only apply nessecary values to the model,
func = map_model(temp_model, cell_or_mod)
sig = inspect.signature(func)
# unpack signature into list for merging all dictionaries
model_args = {
k: (v.default if v.default is not inspect.Parameter.empty else None)
for k, v in sig.parameters.items()
}
# if key is present update the value in the function signature
for key in model_args:
# we only want to update the values with matching keys
if key in weather_args:
model_args[key] = weather_args[key]
try:
model_args.update(parameters)
except NameError:
pass # hits when not sapm or pvsyst
# add optional kwargs, overwrites copies
model_args.update(**model_kwarg)
temperature = func(**model_args)
return temperature