From 0e96b633b5df9fcf2af3be7cd1527f430fc9900b Mon Sep 17 00:00:00 2001 From: Stephen Kaplan Date: Fri, 16 Oct 2020 13:32:00 -0600 Subject: [PATCH 01/20] checkpoint. initial implementation of hayes model. still need to calculate long wave radiation with view factors, etc. --- pvlib/temperature.py | 71 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/pvlib/temperature.py b/pvlib/temperature.py index 1d98736b4f..d83f92a36d 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -3,8 +3,10 @@ PV modules and cells. """ +import math import numpy as np import pandas as pd +from pandas.tseries.frequencies import to_offset from pvlib.tools import sind TEMPERATURE_MODEL_PARAMETERS = { @@ -657,3 +659,72 @@ def fuentes(poa_global, temp_air, wind_speed, noct_installed, module_height=5, sun0 = sun return pd.Series(tmod_array - 273.15, index=poa_global.index, name='tmod') + + +def _calculate_radiative_heat_transfer(module_area, view_factor12, emissivity1, temperature1, emissivity2, temperature2): + r""" + """ + # Stefan-Boltzmann constant + sigma = 5.670374419E-8 # W m^-2 K^-4 + q12 = sigma * module_area * view_factor12 * (emissivity1*(temperature1 ** 4) - emissivity2*(temperature2 ** 4)) + + return q12 + + +def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, t_mod_init=None, emissivity_sky=0.95, + emissivity_ground=0.85, k_c=12.7, k_v=2.0, wind_sensor_height=2.5, z0=0.25, mod_heat_capacity=840): + r""" + for fixed tilt + + for now if initial module temperature not provided, assumes it is ambient temp. as suggested in github issue, + this should really only be allowed if it is middle of night (midnight) and panels are in steady state equilibrium + with environment + + performs best (better than PVsyst model) for small time intervals (< 5 minutes). could also interpolate hourly data + to 5 minute for higher accuracy when only lower res data is available + + units for heat cap dont make sense, also might be optimized for CdTE + + emissivity ground is defaulted to sand value, grass is 0.9 + + convective coefficients are defaulted to those for "hot" climates (Koppen-Geiger Dry B and Temperate C) + for "temperate" climates (Koppen-Geiger Cold D), k_c = 16.5 and k_v = 3.2 + + """ + # TODO validation for length of inputs? + + # infer the time resolution from the inputted time series. first get pandas frequency alias, then convert to seconds + freq = pd.infer_freq(poa_effective.index) + dt = pd.to_timedelta(to_offset(freq)).seconds + + # calculate short wave radiation (from sun) using effective POA (POA accounting for optical losses) + q_short_wave_radiation = module_area * poa_effective + + # calculate the portion of incoming solar irradiance converted to electrical energy + p_out = module_efficiency * module_area * poa_effective + + # adjust wind speed if sensor height not at 2.5 meters + wind_speed_adj = wind_speed * (math.log(2.5 / z0) / math.log(wind_sensor_height / z0)) + + t_mod = np.zeros_like(poa_effective) + t_mod_i = (t_mod_init if t_mod_init is not None else temp_air[0]) + 273.15 + t_mod[0] = t_mod_i + # calculate successive module temperatures for each time stamp + for i in range(len(t_mod) - 1): + # TODO figure these out + # calculate long wave radiation (radiative interactions between module and objects within Earth's atmosphere) + q_mod_sky = 0 # _calculate_radiative_heat_transfer(module_area, ) + q_mod_ground = 0 # _calculate_radiative_heat_transfer(module_area, ) + q_mod_mod = 0 # _calculate_radiative_heat_transfer(module_area, ) + q_long_wave_radiation = q_mod_sky + q_mod_ground + q_mod_mod + + # calculation convective heat transfer + q_convection = (k_c + k_v * wind_speed_adj[i]) * (t_mod_i - temp_air[i] - 273.15) + + # calculate the delta in module temperature, and add it to the current module temperature + t_mod_delta = (dt / mod_heat_capacity) * \ + (q_long_wave_radiation + q_short_wave_radiation[i] + q_convection - p_out[i]) / 34.5 # TODO + t_mod_i += t_mod_delta + t_mod[i + 1] = t_mod_i + + return pd.Series(t_mod - 273.15, index=poa_effective.index, name='tmod') From 364bbd4aa3cffd9398ff7324ca94703b70667000 Mon Sep 17 00:00:00 2001 From: Stephen Kaplan Date: Fri, 16 Oct 2020 17:45:04 -0600 Subject: [PATCH 02/20] committing for initial pull request. still need to write tests, document, etc. --- pvlib/temperature.py | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/pvlib/temperature.py b/pvlib/temperature.py index d83f92a36d..24c779b35e 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -661,19 +661,21 @@ def fuentes(poa_global, temp_air, wind_speed, noct_installed, module_height=5, return pd.Series(tmod_array - 273.15, index=poa_global.index, name='tmod') -def _calculate_radiative_heat_transfer(module_area, view_factor12, emissivity1, temperature1, emissivity2, temperature2): - r""" +def _calculate_radiative_heat(module_area, view_factor, emissivity, temperature1, temperature2): + """ + """ # Stefan-Boltzmann constant sigma = 5.670374419E-8 # W m^-2 K^-4 - q12 = sigma * module_area * view_factor12 * (emissivity1*(temperature1 ** 4) - emissivity2*(temperature2 ** 4)) + q = sigma * module_area * view_factor * emissivity * (temperature1 ** 4 - temperature2 ** 4) - return q12 + return q -def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, t_mod_init=None, emissivity_sky=0.95, - emissivity_ground=0.85, k_c=12.7, k_v=2.0, wind_sensor_height=2.5, z0=0.25, mod_heat_capacity=840): - r""" +def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, module_tilt, t_mod_init=None, + emissivity_sky=0.95, emissivity_ground=0.85, k_c=12.7, k_v=2.0, wind_sensor_height=2.5, z0=0.25, + mod_heat_capacity=840): + """ for fixed tilt for now if initial module temperature not provided, assumes it is ambient temp. as suggested in github issue, @@ -691,35 +693,38 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, t for "temperate" climates (Koppen-Geiger Cold D), k_c = 16.5 and k_v = 3.2 """ - # TODO validation for length of inputs? + # ensure that time series inputs are all of the same length + if not (len(poa_effective) == len(temp_air) and len(temp_air) == len(wind_speed)): + raise ValueError('poa_effective, temp_air, and wind_speed must all be pandas Series of the same size.') # infer the time resolution from the inputted time series. first get pandas frequency alias, then convert to seconds freq = pd.infer_freq(poa_effective.index) dt = pd.to_timedelta(to_offset(freq)).seconds - # calculate short wave radiation (from sun) using effective POA (POA accounting for optical losses) - q_short_wave_radiation = module_area * poa_effective - - # calculate the portion of incoming solar irradiance converted to electrical energy - p_out = module_efficiency * module_area * poa_effective + q_short_wave_radiation = module_area * poa_effective # radiation (from sun) + p_out = module_efficiency * module_area * poa_effective # converted electrical energy # adjust wind speed if sensor height not at 2.5 meters wind_speed_adj = wind_speed * (math.log(2.5 / z0) / math.log(wind_sensor_height / z0)) + # calculate view factors (simplified calculations) + view_factor_mod_sky = (1 + math.cos(module_tilt)) / 2 + view_factor_mod_ground = (1 - math.cos(module_tilt)) / 2 + t_mod = np.zeros_like(poa_effective) t_mod_i = (t_mod_init if t_mod_init is not None else temp_air[0]) + 273.15 t_mod[0] = t_mod_i # calculate successive module temperatures for each time stamp for i in range(len(t_mod) - 1): - # TODO figure these out # calculate long wave radiation (radiative interactions between module and objects within Earth's atmosphere) - q_mod_sky = 0 # _calculate_radiative_heat_transfer(module_area, ) - q_mod_ground = 0 # _calculate_radiative_heat_transfer(module_area, ) - q_mod_mod = 0 # _calculate_radiative_heat_transfer(module_area, ) + t_sky = temp_air[i] - 20 + 273.15 + q_mod_sky = _calculate_radiative_heat(module_area, view_factor_mod_sky, emissivity_sky, t_mod_i, t_sky) + q_mod_ground = _calculate_radiative_heat(module_area, view_factor_mod_ground, emissivity_ground, t_mod_i, t_mod_i) + q_mod_mod = 0 # current assumption is that it is negligible q_long_wave_radiation = q_mod_sky + q_mod_ground + q_mod_mod # calculation convective heat transfer - q_convection = (k_c + k_v * wind_speed_adj[i]) * (t_mod_i - temp_air[i] - 273.15) + q_convection = (k_c + k_v * wind_speed_adj[i]) * ((t_mod_i - 273.15) - temp_air[i]) # calculate the delta in module temperature, and add it to the current module temperature t_mod_delta = (dt / mod_heat_capacity) * \ From 2c560d7807b2c7bbae0ede3cfa98ad6d8c735c4b Mon Sep 17 00:00:00 2001 From: Stephen Kaplan Date: Fri, 16 Oct 2020 18:23:16 -0600 Subject: [PATCH 03/20] updates entries to docs/sphinx/source/api.rst and wrote clear first line in inline documentation of function for sphinx autosummary. --- docs/sphinx/source/api.rst | 1 + pvlib/temperature.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 077a5e121d..e5d8023033 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -236,6 +236,7 @@ PV temperature models temperature.pvsyst_cell temperature.faiman temperature.fuentes + temperature.hayes pvsystem.PVSystem.sapm_celltemp Temperature Model Parameters diff --git a/pvlib/temperature.py b/pvlib/temperature.py index 24c779b35e..13169518d3 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -676,6 +676,10 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, m emissivity_sky=0.95, emissivity_ground=0.85, k_c=12.7, k_v=2.0, wind_sensor_height=2.5, z0=0.25, mod_heat_capacity=840): """ + Calculate module temperature at sub-hourly resolution for fixed tilt systems per the Hayes model. + + [refrence paper] + for fixed tilt for now if initial module temperature not provided, assumes it is ambient temp. as suggested in github issue, From 0179194b3fb103411e02648381f67246442d1209 Mon Sep 17 00:00:00 2001 From: Stephen Kaplan Date: Fri, 16 Oct 2020 18:34:54 -0600 Subject: [PATCH 04/20] fixed complaints from Stickler CI (mostly line length) --- pvlib/temperature.py | 83 +++++++++++++++++++++++++++++++------------- 1 file changed, 58 insertions(+), 25 deletions(-) diff --git a/pvlib/temperature.py b/pvlib/temperature.py index 13169518d3..9a65683a3c 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -661,55 +661,70 @@ def fuentes(poa_global, temp_air, wind_speed, noct_installed, module_height=5, return pd.Series(tmod_array - 273.15, index=poa_global.index, name='tmod') -def _calculate_radiative_heat(module_area, view_factor, emissivity, temperature1, temperature2): +def _calculate_radiative_heat(module_area, view_factor, emissivity, + temperature1, temperature2): """ """ # Stefan-Boltzmann constant sigma = 5.670374419E-8 # W m^-2 K^-4 - q = sigma * module_area * view_factor * emissivity * (temperature1 ** 4 - temperature2 ** 4) + q = sigma * module_area * view_factor * emissivity * \ + (temperature1 ** 4 - temperature2 ** 4) return q -def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, module_tilt, t_mod_init=None, - emissivity_sky=0.95, emissivity_ground=0.85, k_c=12.7, k_v=2.0, wind_sensor_height=2.5, z0=0.25, - mod_heat_capacity=840): +def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, + module_weight, module_tilt, t_mod_init=None, emissivity_sky=0.95, + emissivity_ground=0.85, k_c=12.7, k_v=2.0, wind_sensor_height=2.5, + z0=0.25, mod_heat_capacity=840): """ - Calculate module temperature at sub-hourly resolution for fixed tilt systems per the Hayes model. + Calculate module temperature at sub-hourly resolution for fixed tilt + systems per the Hayes model. [refrence paper] for fixed tilt - for now if initial module temperature not provided, assumes it is ambient temp. as suggested in github issue, - this should really only be allowed if it is middle of night (midnight) and panels are in steady state equilibrium - with environment + for now if initial module temperature not provided, assumes it is ambient + temp. as suggested in github issue, this should really only be allowed if + it is middle of night (midnight) and panels are in steady state + equilibrium with environment - performs best (better than PVsyst model) for small time intervals (< 5 minutes). could also interpolate hourly data - to 5 minute for higher accuracy when only lower res data is available + performs best (better than PVsyst model) for small time intervals + (< 5 minutes). could also interpolate hourly data to 5 minute for higher + accuracy when only lower res data is available units for heat cap dont make sense, also might be optimized for CdTE emissivity ground is defaulted to sand value, grass is 0.9 - convective coefficients are defaulted to those for "hot" climates (Koppen-Geiger Dry B and Temperate C) - for "temperate" climates (Koppen-Geiger Cold D), k_c = 16.5 and k_v = 3.2 + convective coefficients are defaulted to those for "hot" climates + (Koppen-Geiger Dry B and Temperate C) for "temperate" climates + (Koppen-Geiger Cold D), k_c = 16.5 and k_v = 3.2 """ # ensure that time series inputs are all of the same length - if not (len(poa_effective) == len(temp_air) and len(temp_air) == len(wind_speed)): - raise ValueError('poa_effective, temp_air, and wind_speed must all be pandas Series of the same size.') + if not (len(poa_effective) == len(temp_air) and + len(temp_air) == len(wind_speed)): - # infer the time resolution from the inputted time series. first get pandas frequency alias, then convert to seconds + raise ValueError('poa_effective, temp_air, and wind_speed must all be' + ' pandas Series of the same size.') + + # infer the time resolution from the inputted time series. + # first get pandas frequency alias, then convert to seconds freq = pd.infer_freq(poa_effective.index) dt = pd.to_timedelta(to_offset(freq)).seconds - q_short_wave_radiation = module_area * poa_effective # radiation (from sun) - p_out = module_efficiency * module_area * poa_effective # converted electrical energy + # radiation (from sun) + q_short_wave_radiation = module_area * poa_effective + + # converted electrical energy + p_out = module_efficiency * module_area * poa_effective # adjust wind speed if sensor height not at 2.5 meters - wind_speed_adj = wind_speed * (math.log(2.5 / z0) / math.log(wind_sensor_height / z0)) + wind_speed_adj = wind_speed * (math.log(2.5 / z0) / + math.log(wind_sensor_height / z0)) # calculate view factors (simplified calculations) view_factor_mod_sky = (1 + math.cos(module_tilt)) / 2 @@ -720,19 +735,37 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, m t_mod[0] = t_mod_i # calculate successive module temperatures for each time stamp for i in range(len(t_mod) - 1): - # calculate long wave radiation (radiative interactions between module and objects within Earth's atmosphere) + # calculate long wave radiation (radiative interactions between module + # and objects within Earth's atmosphere) t_sky = temp_air[i] - 20 + 273.15 - q_mod_sky = _calculate_radiative_heat(module_area, view_factor_mod_sky, emissivity_sky, t_mod_i, t_sky) - q_mod_ground = _calculate_radiative_heat(module_area, view_factor_mod_ground, emissivity_ground, t_mod_i, t_mod_i) + q_mod_sky = _calculate_radiative_heat( + module_area=module_area, + view_factor=view_factor_mod_sky, + emissivity=emissivity_sky, + temperature1=t_mod_i, + temperature2=t_sky + ) + q_mod_ground = _calculate_radiative_heat( + module_area=module_area, + view_factor=view_factor_mod_ground, + emissivity=emissivity_ground, + temperature1=t_mod_i, + temperature2=t_mod_i + ) q_mod_mod = 0 # current assumption is that it is negligible q_long_wave_radiation = q_mod_sky + q_mod_ground + q_mod_mod # calculation convective heat transfer - q_convection = (k_c + k_v * wind_speed_adj[i]) * ((t_mod_i - 273.15) - temp_air[i]) + q_convection = (k_c + k_v * wind_speed_adj[i]) * \ + ((t_mod_i - 273.15) - temp_air[i]) - # calculate the delta in module temperature, and add it to the current module temperature + # calculate delta in module temp, add to the current module temp + total_heat_transfer = (q_long_wave_radiation + + q_short_wave_radiation[i] + + q_convection - p_out[i]) t_mod_delta = (dt / mod_heat_capacity) * \ - (q_long_wave_radiation + q_short_wave_radiation[i] + q_convection - p_out[i]) / 34.5 # TODO + (1/module_weight) * total_heat_transfer + t_mod_i += t_mod_delta t_mod[i + 1] = t_mod_i From 381249a46837de232ff3d19b8ca871fee5cb016c Mon Sep 17 00:00:00 2001 From: Stephen Kaplan Date: Sat, 17 Oct 2020 09:53:49 -0600 Subject: [PATCH 05/20] initial drat of documented functions --- pvlib/temperature.py | 129 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 108 insertions(+), 21 deletions(-) diff --git a/pvlib/temperature.py b/pvlib/temperature.py index 9a65683a3c..8071954faf 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -664,7 +664,29 @@ def fuentes(poa_global, temp_air, wind_speed, noct_installed, module_height=5, def _calculate_radiative_heat(module_area, view_factor, emissivity, temperature1, temperature2): """ + Calculate radiative heat transfer between two objects. + Parameters + ---------- + module_area : float + Front-side surface area of PV module [m^2] + + view_factor : float + View factor of object 1 with respect to object 2 [unitless] + + emissivity : float + Thermal emissivity [unitless] # TODO there are probably 2 of these values + + temperature1 : float + Temperature of object 1 [C] + + temperature2 : float + + Temperature of object 2 [C] + Returns + ------- + q : float + Radiative heat transfer between object 1 and object 2 [W] """ # Stefan-Boltzmann constant sigma = 5.670374419E-8 # W m^-2 K^-4 @@ -675,34 +697,99 @@ def _calculate_radiative_heat(module_area, view_factor, emissivity, def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, - module_weight, module_tilt, t_mod_init=None, emissivity_sky=0.95, - emissivity_ground=0.85, k_c=12.7, k_v=2.0, wind_sensor_height=2.5, - z0=0.25, mod_heat_capacity=840): + module_weight, module_tilt, mod_heat_capacity=840, t_mod_init=None, + emissivity_sky=0.95, emissivity_ground=0.85, k_c=12.7, k_v=2.0, + wind_sensor_height=2.5, z0=0.25): """ Calculate module temperature at sub-hourly resolution for fixed tilt systems per the Hayes model. - [refrence paper] + The Hayes model [1]_ enables more accurate modeling of module temperature + at time scales less than one hour by introducing a time dependency based + on module heat capacity. The model can only be used for fixed tilt + systems. Additionally, it has only been validated using data from + utility-scale PV systems with CdTe modules. It is more accurate for + time intervals less than 5 minutes. + + Parameters + ---------- + poa_effective : pandas Series + Total incident irradiance adjusted for optical (IAM) losses [W/m^2] + + temp_air : pandas Series + Ambient dry bulb temperature [C] + + wind_speed : pandas Series + Wind speed [m/s] - for fixed tilt + module_efficiency : float + PV module efficiency [decimal] - for now if initial module temperature not provided, assumes it is ambient - temp. as suggested in github issue, this should really only be allowed if - it is middle of night (midnight) and panels are in steady state - equilibrium with environment + module_area : float + Front-side surface area of PV module [m^2] - performs best (better than PVsyst model) for small time intervals - (< 5 minutes). could also interpolate hourly data to 5 minute for higher - accuracy when only lower res data is available + module_weight : float + Weight of PV module [kg] - units for heat cap dont make sense, also might be optimized for CdTE + module_tilt : float + Tilt angle of fixed tilt array [deg] - emissivity ground is defaulted to sand value, grass is 0.9 + mod_heat_capacity : float, default 840 + Specific heat capacity of PV module [J / kg-K]. - convective coefficients are defaulted to those for "hot" climates - (Koppen-Geiger Dry B and Temperate C) for "temperate" climates - (Koppen-Geiger Cold D), k_c = 16.5 and k_v = 3.2 + t_mod_init : float, default None + Initial condition for module temperature [C]. If left as default, + will be set to first value in temp_air (based on the assumption + that if the first timestamp is in the middle of the night, the + module would be in steady-state equilibrium with the environment + emissivity_sky : float, default 0.95 + Thermal emissivity of sky [unitless]. + + emissivity_ground : float, default 0.85 + Thermal emissivity of ground [unitless]. Default value is suggested + value for sand. Suggested value for grass is 0.9. + + k_c : float, default 12.7 + Free convective heat coefficient. Defaults to value for "hot" + climates (climates closest to Koppen-Geiger Dry B and Temperate C + zones). Suggested value for "temperate" climates (climates closest + to Koppen-Geiger Cold D zones) is 16.5 + + k_v : float, default 2.0 + Forced convective heat coefficient. Defaults to value for "hot" + climates (climates closest to Koppen-Geiger Dry B and Temperate C + zones). Suggested value for "temperate" climates (climates closest + to Koppen-Geiger Cold D zones) 3.2 + + wind_sensor_height : float, default 2.5 + Height of wind sensor used to measure wind_speed [m] + + z0 : float, default 0.25 + Davenport-Wieringa roughness length [m]. Default value chosen in + white-paper to minimize error. + + Returns + ------- + tmod : pandas Series + The modeled module temperature [C] + + Notes + ----- + The Hayes model for module temperature :math:`T_{t + dt}`, for a given + timestamp is given by + + .. math:: + :label: Hayes + + T_{t + dt} = \frac{dt}{C_{mod}} + + References + ---------- + .. [1] W. Hayes and L. Ngan, "A Time-Dependent Model for CdTe PV Module + Temperature in Utility-Scale Systems," in IEEE Journal of + Photovoltaics, vol. 5, no. 1, pp. 238-242, Jan. 2015, doi: + 10.1109/JPHOTOV.2014.2361653. """ # ensure that time series inputs are all of the same length if not (len(poa_effective) == len(temp_air) and @@ -727,8 +814,8 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, math.log(wind_sensor_height / z0)) # calculate view factors (simplified calculations) - view_factor_mod_sky = (1 + math.cos(module_tilt)) / 2 - view_factor_mod_ground = (1 - math.cos(module_tilt)) / 2 + view_factor_mod_sky = (1 + math.cos(math.radians(module_tilt))) / 2 + view_factor_mod_ground = (1 - math.cos(math.radians(module_tilt))) / 2 t_mod = np.zeros_like(poa_effective) t_mod_i = (t_mod_init if t_mod_init is not None else temp_air[0]) + 273.15 @@ -753,7 +840,7 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, temperature2=t_mod_i ) q_mod_mod = 0 # current assumption is that it is negligible - q_long_wave_radiation = q_mod_sky + q_mod_ground + q_mod_mod + q_long_wave_radiation = -1*(q_mod_sky + q_mod_ground + q_mod_mod) # calculation convective heat transfer q_convection = (k_c + k_v * wind_speed_adj[i]) * \ @@ -761,7 +848,7 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, # calculate delta in module temp, add to the current module temp total_heat_transfer = (q_long_wave_radiation + - q_short_wave_radiation[i] + + q_short_wave_radiation[i] - q_convection - p_out[i]) t_mod_delta = (dt / mod_heat_capacity) * \ (1/module_weight) * total_heat_transfer From f8927ff5bca66e545878071e58d45d43e481277b Mon Sep 17 00:00:00 2001 From: Stephen Kaplan Date: Sat, 17 Oct 2020 09:54:41 -0600 Subject: [PATCH 06/20] initial drat of documented functions --- pvlib/temperature.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pvlib/temperature.py b/pvlib/temperature.py index 8071954faf..556452923c 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -675,7 +675,8 @@ def _calculate_radiative_heat(module_area, view_factor, emissivity, View factor of object 1 with respect to object 2 [unitless] emissivity : float - Thermal emissivity [unitless] # TODO there are probably 2 of these values + # TODO there are probably 2 of these values + Thermal emissivity [unitless] temperature1 : float Temperature of object 1 [C] From f39c338b7d8091ddebd6417d976c63d82197520a Mon Sep 17 00:00:00 2001 From: Stephen Kaplan Date: Sat, 17 Oct 2020 10:09:30 -0600 Subject: [PATCH 07/20] fixed documentation --- pvlib/temperature.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pvlib/temperature.py b/pvlib/temperature.py index 556452923c..d312468b46 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -775,16 +775,6 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, tmod : pandas Series The modeled module temperature [C] - Notes - ----- - The Hayes model for module temperature :math:`T_{t + dt}`, for a given - timestamp is given by - - .. math:: - :label: Hayes - - T_{t + dt} = \frac{dt}{C_{mod}} - References ---------- .. [1] W. Hayes and L. Ngan, "A Time-Dependent Model for CdTe PV Module From bfdcf0e44d4b51b7858cb389e3ff38a7db9e9450 Mon Sep 17 00:00:00 2001 From: Stephen Kaplan Date: Sat, 17 Oct 2020 10:21:37 -0600 Subject: [PATCH 08/20] placeholder unit tests, some documentation fixes. --- pvlib/temperature.py | 12 +++++++----- pvlib/tests/test_temperature.py | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/pvlib/temperature.py b/pvlib/temperature.py index d312468b46..8bed49de69 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -676,14 +676,14 @@ def _calculate_radiative_heat(module_area, view_factor, emissivity, emissivity : float # TODO there are probably 2 of these values - Thermal emissivity [unitless] + Thermal emissivity [unitless]. Must be between 0 and 1. temperature1 : float - Temperature of object 1 [C] + Temperature of object 1 [K] temperature2 : float - Temperature of object 2 [C] + Temperature of object 2 [K] Returns ------- q : float @@ -745,11 +745,12 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, module would be in steady-state equilibrium with the environment emissivity_sky : float, default 0.95 - Thermal emissivity of sky [unitless]. + Thermal emissivity of sky [unitless]. Must be between 0 and 1. emissivity_ground : float, default 0.85 Thermal emissivity of ground [unitless]. Default value is suggested - value for sand. Suggested value for grass is 0.9. + value for sand. Suggested value for grass is 0.9. Must be between + 0 and 1. k_c : float, default 12.7 Free convective heat coefficient. Defaults to value for "hot" @@ -823,6 +824,7 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, temperature1=t_mod_i, temperature2=t_sky ) + # TODO paper indicates temps equal, but that yields zero q q_mod_ground = _calculate_radiative_heat( module_area=module_area, view_factor=view_factor_mod_ground, diff --git a/pvlib/tests/test_temperature.py b/pvlib/tests/test_temperature.py index 245c5d2807..06248e2d2a 100644 --- a/pvlib/tests/test_temperature.py +++ b/pvlib/tests/test_temperature.py @@ -190,3 +190,21 @@ def test_fuentes(filename, inoct): night_difference = expected_tcell[is_night] - actual_tcell[is_night] assert night_difference.max() < 6 assert night_difference.min() > 0 + + +def test__calculate_radiative_heat(): + # TODO placeholder until final model is validated + q = temperature._calculate_radiative_heat( + module_area=2.47, + view_factor=0.5, + emissivity=0.5, + temperature1=30 + 273.15, + temperature2=10 + 273.15 + ) + assert round(q, 5) == 70.65021 + + +def test_hayes(): + # TODO placeholder until final model is validated + pass + From 840beb74f61850772339dbdbd715536f0b9e1629 Mon Sep 17 00:00:00 2001 From: Stephen Kaplan Date: Sun, 18 Oct 2020 23:31:57 -0600 Subject: [PATCH 09/20] added main placeholder test and temporarily took out module area in short wave heat transfer calculation --- pvlib/temperature.py | 2 +- pvlib/tests/test_temperature.py | 55 +++++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/pvlib/temperature.py b/pvlib/temperature.py index 8bed49de69..9a24892203 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -796,7 +796,7 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, dt = pd.to_timedelta(to_offset(freq)).seconds # radiation (from sun) - q_short_wave_radiation = module_area * poa_effective + q_short_wave_radiation = poa_effective # module_area * poa_effective # converted electrical energy p_out = module_efficiency * module_area * poa_effective diff --git a/pvlib/tests/test_temperature.py b/pvlib/tests/test_temperature.py index 06248e2d2a..623ef5bffd 100644 --- a/pvlib/tests/test_temperature.py +++ b/pvlib/tests/test_temperature.py @@ -5,7 +5,7 @@ from conftest import DATA_DIR, assert_series_equal from numpy.testing import assert_allclose -from pvlib import temperature +from pvlib import temperature, location, irradiance, iam @pytest.fixture @@ -206,5 +206,56 @@ def test__calculate_radiative_heat(): def test_hayes(): # TODO placeholder until final model is validated - pass + # simulate psm3 data + data_psm3 = [ + {'Latitude': 39.66, 'Longitude': -105.207}, + pd.DataFrame( + data={ + 'DNI': [0, 163, 133, 189], + 'DHI': [0, 4, 12, 16], + 'GHI': [0, 7, 16, 25], + 'Temperature': [-13.2, -13.1, -13.1, -13], + 'Wind Speed': [1.6, 1.7, 1.7, 1.7] + }, + index=pd.date_range('2019-01-01 07:25:00', + '2019-01-01 07:40:00', + freq='5min') + ) + ] + + # data preparation + module_tilt = 30 + module_azimuth = 180 + site = location.Location( + latitude=data_psm3[0]['Latitude'], + longitude=data_psm3[0]['Longitude'], + tz='MST' + ) + solar_position = site.get_solarposition(times=data_psm3[1].index) + poa_global = irradiance.get_total_irradiance( + surface_tilt=module_tilt, + surface_azimuth=module_azimuth, + dni=data_psm3[1]['DNI'], + ghi=data_psm3[1]['GHI'], + dhi=data_psm3[1]['DHI'], + solar_zenith=solar_position['apparent_zenith'], + solar_azimuth=solar_position['azimuth'] + )['poa_global'] + temp_air = data_psm3[1]['Temperature'] + wind_speed = data_psm3[1]['Wind Speed'] + + # 1. Calculate module temp with new model + aoi = irradiance.aoi(module_tilt, module_azimuth, + solar_position['zenith'], + solar_position['azimuth']) + poa_effective = poa_global.multiply(iam.ashrae(aoi)) + module_efficiency = 0.176 + module_area = 2.47 # m^2 + module_weight = 34.5 + tmod_hayes = temperature.hayes(poa_effective, temp_air, wind_speed, + module_efficiency, module_area, + module_weight, module_tilt) + + assert [round(t, 2) for t in tmod_hayes.values] == \ + [-13.20, -7.81, -8.98, -9.85] From b4be14277fb253071976033e2e4b9d23ec8f8b7e Mon Sep 17 00:00:00 2001 From: Stephen Kaplan Date: Sun, 18 Oct 2020 23:38:14 -0600 Subject: [PATCH 10/20] made fix for SticklerCI --- pvlib/temperature.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/temperature.py b/pvlib/temperature.py index 9a24892203..6868048400 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -796,7 +796,7 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, dt = pd.to_timedelta(to_offset(freq)).seconds # radiation (from sun) - q_short_wave_radiation = poa_effective # module_area * poa_effective + q_short_wave_radiation = poa_effective # module_area * poa_effective # converted electrical energy p_out = module_efficiency * module_area * poa_effective From 2399607b6828f960086fd460a9e1eaa6735d2197 Mon Sep 17 00:00:00 2001 From: Stephen Kaplan Date: Sun, 18 Oct 2020 23:42:34 -0600 Subject: [PATCH 11/20] fixed unit test --- pvlib/tests/test_temperature.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/tests/test_temperature.py b/pvlib/tests/test_temperature.py index 623ef5bffd..1055dbe66c 100644 --- a/pvlib/tests/test_temperature.py +++ b/pvlib/tests/test_temperature.py @@ -258,4 +258,4 @@ def test_hayes(): module_weight, module_tilt) assert [round(t, 2) for t in tmod_hayes.values] == \ - [-13.20, -7.81, -8.98, -9.85] + [-13.20, -14.81, -15.98, -16.85] From 1e138c0b5773f08143074e513e0e636e2c73c2e0 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 24 Jun 2021 12:36:22 -0600 Subject: [PATCH 12/20] cleanup --- pvlib/temperature.py | 111 +++++++++++++++++++++++++------------------ 1 file changed, 65 insertions(+), 46 deletions(-) diff --git a/pvlib/temperature.py b/pvlib/temperature.py index 690b767027..64735d050f 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -719,8 +719,8 @@ def fuentes(poa_global, temp_air, wind_speed, noct_installed, module_height=5, return pd.Series(tmod_array - 273.15, index=poa_global.index, name='tmod') -def _calculate_radiative_heat(module_area, view_factor, emissivity, - temperature1, temperature2): +def _calculate_radiative_heat(module_area, view_factor, emissivity1, + emissivity2, temperature1, temperature2): """ Calculate radiative heat transfer between two objects. @@ -732,16 +732,18 @@ def _calculate_radiative_heat(module_area, view_factor, emissivity, view_factor : float View factor of object 1 with respect to object 2 [unitless] - emissivity : float - # TODO there are probably 2 of these values - Thermal emissivity [unitless]. Must be between 0 and 1. + emissivity1 : float + Thermal emissivity of object 1 [unitless]. Must be between 0 and 1. + + emissivity2 : float + Thermal emissivity of object 2 [unitless]. Must be between 0 and 1. temperature1 : float Temperature of object 1 [K] temperature2 : float - Temperature of object 2 [K] + Returns ------- q : float @@ -749,16 +751,16 @@ def _calculate_radiative_heat(module_area, view_factor, emissivity, """ # Stefan-Boltzmann constant sigma = 5.670374419E-8 # W m^-2 K^-4 - q = sigma * module_area * view_factor * emissivity * \ - (temperature1 ** 4 - temperature2 ** 4) + q = sigma * module_area * view_factor * \ + (emissivity1 * temperature1 ** 4 - emissivity2 * temperature2 ** 4) return q def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, - module_weight, module_tilt, mod_heat_capacity=840, t_mod_init=None, - emissivity_sky=0.95, emissivity_ground=0.85, k_c=12.7, k_v=2.0, - wind_sensor_height=2.5, z0=0.25): + module_weight, surface_tilt, emissivity_module, emissivity_sky=0.95, + emissivity_ground=0.85, heat_capacity=840, t_mod_init=None, k_c=12.7, + k_v=2.0, wind_sensor_height=2.5, z0=0.25): """ Calculate module temperature at sub-hourly resolution for fixed tilt systems per the Hayes model. @@ -770,15 +772,21 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, utility-scale PV systems with CdTe modules. It is more accurate for time intervals less than 5 minutes. + .. warning:: + This model was validated using data from systems built prior to 2012. + Using module parameters (area, efficiency, weight) for more recent + First Solar modules may not produce realistic temperature estimates. + + Parameters ---------- - poa_effective : pandas Series + poa_effective : pandas.Series Total incident irradiance adjusted for optical (IAM) losses [W/m^2] - temp_air : pandas Series + temp_air : pandas.Series Ambient dry bulb temperature [C] - wind_speed : pandas Series + wind_speed : pandas.Series Wind speed [m/s] module_efficiency : float @@ -790,17 +798,11 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, module_weight : float Weight of PV module [kg] - module_tilt : float + surface_tilt : float Tilt angle of fixed tilt array [deg] - mod_heat_capacity : float, default 840 - Specific heat capacity of PV module [J / kg-K]. - - t_mod_init : float, default None - Initial condition for module temperature [C]. If left as default, - will be set to first value in temp_air (based on the assumption - that if the first timestamp is in the middle of the night, the - module would be in steady-state equilibrium with the environment + emissivity_module : float + Thermal emissivity of the module [unitless]. Must be between 0 and 1. emissivity_sky : float, default 0.95 Thermal emissivity of sky [unitless]. Must be between 0 and 1. @@ -810,6 +812,16 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, value for sand. Suggested value for grass is 0.9. Must be between 0 and 1. + heat_capacity : float, default 840 + Specific heat capacity of PV module [J / kg-K]. + The default value is that of glass. + + t_mod_init : float, default None + Initial condition for module temperature [C]. If left as default, + will be set to first value in temp_air based on the assumption + that if the first timestamp is in the middle of the night, the + module would be in steady-state equilibrium with the environment. + k_c : float, default 12.7 Free convective heat coefficient. Defaults to value for "hot" climates (climates closest to Koppen-Geiger Dry B and Temperate C @@ -831,7 +843,7 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, Returns ------- - tmod : pandas Series + tmod : pandas.Series The modeled module temperature [C] References @@ -854,55 +866,62 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, dt = pd.to_timedelta(to_offset(freq)).seconds # radiation (from sun) - q_short_wave_radiation = poa_effective # module_area * poa_effective + q_short_wave_radiation = module_area * poa_effective # converted electrical energy - p_out = module_efficiency * module_area * poa_effective + p_out = module_efficiency * q_short_wave_radiation # adjust wind speed if sensor height not at 2.5 meters wind_speed_adj = wind_speed * (math.log(2.5 / z0) / math.log(wind_sensor_height / z0)) + # convert C to K for convenience + temp_air = temp_air + 273.15 + + # sky temperature assumed to be constant offset from ambient (see Table 1) + t_sky = temp_air - 20 + # calculate view factors (simplified calculations) - view_factor_mod_sky = (1 + math.cos(math.radians(module_tilt))) / 2 - view_factor_mod_ground = (1 - math.cos(math.radians(module_tilt))) / 2 + view_factor_mod_sky = (1 + math.cos(math.radians(surface_tilt))) / 2 + view_factor_mod_ground = (1 - math.cos(math.radians(surface_tilt))) / 2 t_mod = np.zeros_like(poa_effective) - t_mod_i = (t_mod_init if t_mod_init is not None else temp_air[0]) + 273.15 + t_mod_i = t_mod_init + 273.15 if t_mod_init is not None else temp_air[0] t_mod[0] = t_mod_i # calculate successive module temperatures for each time stamp for i in range(len(t_mod) - 1): # calculate long wave radiation (radiative interactions between module # and objects within Earth's atmosphere) - t_sky = temp_air[i] - 20 + 273.15 q_mod_sky = _calculate_radiative_heat( module_area=module_area, view_factor=view_factor_mod_sky, - emissivity=emissivity_sky, + emissivity1=emissivity_module, + emissivity2=emissivity_sky, temperature1=t_mod_i, - temperature2=t_sky + temperature2=t_sky[i], ) - # TODO paper indicates temps equal, but that yields zero q q_mod_ground = _calculate_radiative_heat( module_area=module_area, view_factor=view_factor_mod_ground, - emissivity=emissivity_ground, + emissivity1=emissivity_module, + emissivity2=emissivity_ground, temperature1=t_mod_i, temperature2=t_mod_i ) q_mod_mod = 0 # current assumption is that it is negligible - q_long_wave_radiation = -1*(q_mod_sky + q_mod_ground + q_mod_mod) - - # calculation convective heat transfer - q_convection = (k_c + k_v * wind_speed_adj[i]) * \ - ((t_mod_i - 273.15) - temp_air[i]) - - # calculate delta in module temp, add to the current module temp - total_heat_transfer = (q_long_wave_radiation + - q_short_wave_radiation[i] - - q_convection - p_out[i]) - t_mod_delta = (dt / mod_heat_capacity) * \ - (1/module_weight) * total_heat_transfer + # Eq 4 + q_long_wave_radiation = q_mod_sky + q_mod_ground + q_mod_mod + + # calculation convective heat transfer (Eq 6) + q_convection = (k_c + k_v*wind_speed_adj[i]) * (t_mod_i - temp_air[i]) + + # calculate delta in module temp, add to the current module temp. + # Eq 2 seems to get the sign wrong for some terms; corrected here + total_heat_transfer = ( + - q_long_wave_radiation + q_short_wave_radiation[i] + - q_convection - p_out[i] + ) + t_mod_delta = dt / (module_weight*heat_capacity) * total_heat_transfer t_mod_i += t_mod_delta t_mod[i + 1] = t_mod_i From a41337e85b4d3fb1458a9f287c87d0de5fce42b0 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 28 Sep 2022 14:58:41 -0400 Subject: [PATCH 13/20] remove obsolete api.rst entry --- docs/sphinx/source/api.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 2abb7f91d6..0af200e67c 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -236,7 +236,6 @@ PV temperature models temperature.pvsyst_cell temperature.faiman temperature.fuentes - temperature.hayes temperature.ross temperature.noct_sam pvsystem.PVSystem.get_cell_temperature From 5203233ced6ebf0edcf4d282cede2c5dc41ceea2 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 28 Sep 2022 15:00:37 -0400 Subject: [PATCH 14/20] add to new api listing --- docs/sphinx/source/reference/pv_modeling.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sphinx/source/reference/pv_modeling.rst b/docs/sphinx/source/reference/pv_modeling.rst index 31c380c1bb..17656cdf20 100644 --- a/docs/sphinx/source/reference/pv_modeling.rst +++ b/docs/sphinx/source/reference/pv_modeling.rst @@ -41,6 +41,7 @@ PV temperature models temperature.pvsyst_cell temperature.faiman temperature.fuentes + temperature.hayes temperature.ross temperature.noct_sam temperature.prilliman From ee2388dabdd85a041c5202fd1997a47de30ab2f9 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 28 Sep 2022 15:35:26 -0400 Subject: [PATCH 15/20] some cleanup --- pvlib/temperature.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/pvlib/temperature.py b/pvlib/temperature.py index 853ecf6786..31585f68ae 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -7,7 +7,7 @@ import numpy as np import pandas as pd from pandas.tseries.frequencies import to_offset -from pvlib.tools import sind +from pvlib.tools import sind, cosd from pvlib._deprecation import warn_deprecated from pvlib.tools import _get_sample_intervals import scipy @@ -842,7 +842,7 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, z0 : float, default 0.25 Davenport-Wieringa roughness length [m]. Default value chosen in - white-paper to minimize error. + [1]_ to minimize error. Returns ------- @@ -853,8 +853,8 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, ---------- .. [1] W. Hayes and L. Ngan, "A Time-Dependent Model for CdTe PV Module Temperature in Utility-Scale Systems," in IEEE Journal of - Photovoltaics, vol. 5, no. 1, pp. 238-242, Jan. 2015, doi: - 10.1109/JPHOTOV.2014.2361653. + Photovoltaics, vol. 5, no. 1, pp. 238-242, Jan. 2015, + :doi:`10.1109/JPHOTOV.2014.2361653`. """ # ensure that time series inputs are all of the same length if not (len(poa_effective) == len(temp_air) and @@ -885,8 +885,18 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, t_sky = temp_air - 20 # calculate view factors (simplified calculations) - view_factor_mod_sky = (1 + math.cos(math.radians(surface_tilt))) / 2 - view_factor_mod_ground = (1 - math.cos(math.radians(surface_tilt))) / 2 + # TODO: from Hayes & Ngan: + # + # The view factor represents the percentage of the hemispherical + # dome viewed from object one (the PV module) that is + # occupied by object two. For this case, the reference point on + # the module is assumed to be at the midpoint of the module + # row height. + # + # So these simplified view factor equations are not wholly consistent + # with the reference. + view_factor_mod_sky = (1 + cosd(surface_tilt)) / 2 + view_factor_mod_ground = (1 - cosd(surface_tilt)) / 2 t_mod = np.zeros_like(poa_effective) t_mod_i = t_mod_init + 273.15 if t_mod_init is not None else temp_air[0] From d2ad4e1778a58f16dccec3b325e32af8893a3317 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 28 Sep 2022 16:25:11 -0400 Subject: [PATCH 16/20] more cleanup --- pvlib/temperature.py | 64 +++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/pvlib/temperature.py b/pvlib/temperature.py index 31585f68ae..5d4099d6d2 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -3,10 +3,8 @@ PV modules and cells. """ -import math import numpy as np import pandas as pd -from pandas.tseries.frequencies import to_offset from pvlib.tools import sind, cosd from pvlib._deprecation import warn_deprecated from pvlib.tools import _get_sample_intervals @@ -761,8 +759,8 @@ def _calculate_radiative_heat(module_area, view_factor, emissivity1, def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, - module_weight, surface_tilt, emissivity_module, emissivity_sky=0.95, - emissivity_ground=0.85, heat_capacity=840, t_mod_init=None, k_c=12.7, + module_mass, surface_tilt, module_emissivity, sky_emissivity=0.95, + ground_emissivity=0.85, heat_capacity=840, t_mod_init=None, k_c=12.7, k_v=2.0, wind_sensor_height=2.5, z0=0.25): """ Calculate module temperature at sub-hourly resolution for fixed tilt @@ -773,17 +771,17 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, on module heat capacity. The model can only be used for fixed tilt systems. Additionally, it has only been validated using data from utility-scale PV systems with CdTe modules. It is more accurate for - time intervals less than 5 minutes. + time intervals less than 5 minutes. For data with larger time steps, + [1]_ recommends downscaling the inputs with linear interpolation. .. warning:: This model was validated using data from systems built prior to 2012. Using module parameters (area, efficiency, weight) for more recent First Solar modules may not produce realistic temperature estimates. - Parameters ---------- - poa_effective : pandas.Series + poa_global : pandas.Series Total incident irradiance adjusted for optical (IAM) losses [W/m^2] temp_air : pandas.Series @@ -798,19 +796,19 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, module_area : float Front-side surface area of PV module [m^2] - module_weight : float - Weight of PV module [kg] + module_mass : float + Mass of PV module [kg] surface_tilt : float Tilt angle of fixed tilt array [deg] - emissivity_module : float + module_emissivity : float Thermal emissivity of the module [unitless]. Must be between 0 and 1. - emissivity_sky : float, default 0.95 + sky_emissivity : float, default 0.95 Thermal emissivity of sky [unitless]. Must be between 0 and 1. - emissivity_ground : float, default 0.85 + ground_emissivity : float, default 0.85 Thermal emissivity of ground [unitless]. Default value is suggested value for sand. Suggested value for grass is 0.9. Must be between 0 and 1. @@ -849,6 +847,17 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, tmod : pandas.Series The modeled module temperature [C] + Notes + ----- + For simplicity, this implementation calculates radiative view factors + slightly differently from [1]_ in that the sky and ground view factors + are not affected by adjacent rows in the array. + + Additionally, implementation corrects two supposed errors in the reference: + + 1. Eq 2: the signs of some terms are corrected. + 2. Eq 3: ``POA_eff`` is multiplied by ``A``. + References ---------- .. [1] W. Hayes and L. Ngan, "A Time-Dependent Model for CdTe PV Module @@ -856,17 +865,8 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, Photovoltaics, vol. 5, no. 1, pp. 238-242, Jan. 2015, :doi:`10.1109/JPHOTOV.2014.2361653`. """ - # ensure that time series inputs are all of the same length - if not (len(poa_effective) == len(temp_air) and - len(temp_air) == len(wind_speed)): - - raise ValueError('poa_effective, temp_air, and wind_speed must all be' - ' pandas Series of the same size.') - - # infer the time resolution from the inputted time series. - # first get pandas frequency alias, then convert to seconds - freq = pd.infer_freq(poa_effective.index) - dt = pd.to_timedelta(to_offset(freq)).seconds + dt_seconds = poa_effective.index.to_series().diff().dt.total_seconds() + dt_seconds.values[0] = dt_seconds.values[1] # simplicity # radiation (from sun) q_short_wave_radiation = module_area * poa_effective @@ -875,8 +875,8 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, p_out = module_efficiency * q_short_wave_radiation # adjust wind speed if sensor height not at 2.5 meters - wind_speed_adj = wind_speed * (math.log(2.5 / z0) / - math.log(wind_sensor_height / z0)) + wind_speed_adj = wind_speed * (np.log(2.5 / z0) / + np.log(wind_sensor_height / z0)) # convert C to K for convenience temp_air = temp_air + 273.15 @@ -894,7 +894,8 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, # row height. # # So these simplified view factor equations are not wholly consistent - # with the reference. + # with the reference. Implementing the real VF calculation would + # require additional inputs (gcr, at least). view_factor_mod_sky = (1 + cosd(surface_tilt)) / 2 view_factor_mod_ground = (1 - cosd(surface_tilt)) / 2 @@ -908,16 +909,16 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, q_mod_sky = _calculate_radiative_heat( module_area=module_area, view_factor=view_factor_mod_sky, - emissivity1=emissivity_module, - emissivity2=emissivity_sky, + emissivity1=module_emissivity, + emissivity2=sky_emissivity, temperature1=t_mod_i, temperature2=t_sky[i], ) q_mod_ground = _calculate_radiative_heat( module_area=module_area, view_factor=view_factor_mod_ground, - emissivity1=emissivity_module, - emissivity2=emissivity_ground, + emissivity1=module_emissivity, + emissivity2=ground_emissivity, temperature1=t_mod_i, temperature2=t_mod_i ) @@ -934,7 +935,8 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, - q_long_wave_radiation + q_short_wave_radiation[i] - q_convection - p_out[i] ) - t_mod_delta = dt / (module_weight*heat_capacity) * total_heat_transfer + dt = dt_seconds[i] + t_mod_delta = dt / (module_mass*heat_capacity) * total_heat_transfer t_mod_i += t_mod_delta t_mod[i + 1] = t_mod_i From 41da4c9284d56710f7ada5cc70258fa0e87f6a82 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 28 Sep 2022 17:24:20 -0400 Subject: [PATCH 17/20] more cleanup --- pvlib/temperature.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pvlib/temperature.py b/pvlib/temperature.py index 5d4099d6d2..e43bae64af 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -758,7 +758,7 @@ def _calculate_radiative_heat(module_area, view_factor, emissivity1, return q -def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, +def hayes(poa_global, temp_air, wind_speed, module_efficiency, module_area, module_mass, surface_tilt, module_emissivity, sky_emissivity=0.95, ground_emissivity=0.85, heat_capacity=840, t_mod_init=None, k_c=12.7, k_v=2.0, wind_sensor_height=2.5, z0=0.25): @@ -804,6 +804,8 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, module_emissivity : float Thermal emissivity of the module [unitless]. Must be between 0 and 1. + No guidance for this value was given in [1]_, but the analogous + parameter in :py:func:`fuentes` defaults to 0.84. sky_emissivity : float, default 0.95 Thermal emissivity of sky [unitless]. Must be between 0 and 1. @@ -865,11 +867,11 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, Photovoltaics, vol. 5, no. 1, pp. 238-242, Jan. 2015, :doi:`10.1109/JPHOTOV.2014.2361653`. """ - dt_seconds = poa_effective.index.to_series().diff().dt.total_seconds() + dt_seconds = poa_global.index.to_series().diff().dt.total_seconds() dt_seconds.values[0] = dt_seconds.values[1] # simplicity # radiation (from sun) - q_short_wave_radiation = module_area * poa_effective + q_short_wave_radiation = module_area * poa_global # converted electrical energy p_out = module_efficiency * q_short_wave_radiation @@ -899,7 +901,7 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, view_factor_mod_sky = (1 + cosd(surface_tilt)) / 2 view_factor_mod_ground = (1 - cosd(surface_tilt)) / 2 - t_mod = np.zeros_like(poa_effective) + t_mod = np.zeros_like(poa_global) t_mod_i = t_mod_init + 273.15 if t_mod_init is not None else temp_air[0] t_mod[0] = t_mod_i # calculate successive module temperatures for each time stamp @@ -937,11 +939,10 @@ def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, ) dt = dt_seconds[i] t_mod_delta = dt / (module_mass*heat_capacity) * total_heat_transfer - t_mod_i += t_mod_delta t_mod[i + 1] = t_mod_i - return pd.Series(t_mod - 273.15, index=poa_effective.index, name='tmod') + return pd.Series(t_mod - 273.15, index=poa_global.index, name='tmod') def _adj_for_mounting_standoff(x): From 4ea7671af8b54c97158939ff147c31c40f090060 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 28 Sep 2022 17:24:45 -0400 Subject: [PATCH 18/20] fix pesky integer truncation bug --- pvlib/temperature.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/temperature.py b/pvlib/temperature.py index e43bae64af..0e0ccdbc9a 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -901,7 +901,7 @@ def hayes(poa_global, temp_air, wind_speed, module_efficiency, module_area, view_factor_mod_sky = (1 + cosd(surface_tilt)) / 2 view_factor_mod_ground = (1 - cosd(surface_tilt)) / 2 - t_mod = np.zeros_like(poa_global) + t_mod = np.zeros_like(poa_global, dtype=np.float64) t_mod_i = t_mod_init + 273.15 if t_mod_init is not None else temp_air[0] t_mod[0] = t_mod_i # calculate successive module temperatures for each time stamp From db57abb18b3825509a927a217a73102e97b15059 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 28 Sep 2022 17:32:46 -0400 Subject: [PATCH 19/20] a couple very basic tests --- pvlib/tests/test_temperature.py | 96 ++++++++++----------------------- 1 file changed, 29 insertions(+), 67 deletions(-) diff --git a/pvlib/tests/test_temperature.py b/pvlib/tests/test_temperature.py index a08ed86406..7cc06c2f02 100644 --- a/pvlib/tests/test_temperature.py +++ b/pvlib/tests/test_temperature.py @@ -210,73 +210,35 @@ def test_fuentes(filename, inoct): assert night_difference.min() > 0 -def test__calculate_radiative_heat(): - # TODO placeholder until final model is validated - q = temperature._calculate_radiative_heat( - module_area=2.47, - view_factor=0.5, - emissivity=0.5, - temperature1=30 + 273.15, - temperature2=10 + 273.15 - ) - assert round(q, 5) == 70.65021 - - -def test_hayes(): - # TODO placeholder until final model is validated - - # simulate psm3 data - data_psm3 = [ - {'Latitude': 39.66, 'Longitude': -105.207}, - pd.DataFrame( - data={ - 'DNI': [0, 163, 133, 189], - 'DHI': [0, 4, 12, 16], - 'GHI': [0, 7, 16, 25], - 'Temperature': [-13.2, -13.1, -13.1, -13], - 'Wind Speed': [1.6, 1.7, 1.7, 1.7] - }, - index=pd.date_range('2019-01-01 07:25:00', - '2019-01-01 07:40:00', - freq='5min') - ) - ] - - # data preparation - module_tilt = 30 - module_azimuth = 180 - site = location.Location( - latitude=data_psm3[0]['Latitude'], - longitude=data_psm3[0]['Longitude'], - tz='MST' - ) - solar_position = site.get_solarposition(times=data_psm3[1].index) - poa_global = irradiance.get_total_irradiance( - surface_tilt=module_tilt, - surface_azimuth=module_azimuth, - dni=data_psm3[1]['DNI'], - ghi=data_psm3[1]['GHI'], - dhi=data_psm3[1]['DHI'], - solar_zenith=solar_position['apparent_zenith'], - solar_azimuth=solar_position['azimuth'] - )['poa_global'] - temp_air = data_psm3[1]['Temperature'] - wind_speed = data_psm3[1]['Wind Speed'] - - # 1. Calculate module temp with new model - aoi = irradiance.aoi(module_tilt, module_azimuth, - solar_position['zenith'], - solar_position['azimuth']) - poa_effective = poa_global.multiply(iam.ashrae(aoi)) - module_efficiency = 0.176 - module_area = 2.47 # m^2 - module_weight = 34.5 - tmod_hayes = temperature.hayes(poa_effective, temp_air, wind_speed, - module_efficiency, module_area, - module_weight, module_tilt) - - assert [round(t, 2) for t in tmod_hayes.values] == \ - [-13.20, -14.81, -15.98, -16.85] +@pytest.fixture +def hayes_data(): + index = pd.date_range('2019-06-01 12:00', freq='T', periods=5) + df = pd.DataFrame({ + 'poa_global': [600, 700, 100, 800, 900], + 'temp_air': [20, 21, 22, 23, 24], + 'wind_speed': [1, 2, 1, 2, 1], + }, index=index).astype(float) + return df + + +def test_hayes(hayes_data): + out = temperature.hayes(**hayes_data, module_efficiency=0.160, + module_area=0.72, module_mass=12, surface_tilt=20, + module_emissivity=0.84) + expected = pd.Series([20, 21.9448677, 24.1349903, 24.0457299, 26.5799448], + index=hayes_data.index, name='tmod') + assert_series_equal(out, expected) + + +def test_hayes_nan(hayes_data): + df = hayes_data.copy() + df['poa_global'].values[2] = np.nan + expected = pd.Series([20, 21.9448677, 24.1349903, np.nan, np.nan], + index=hayes_data.index, name='tmod') + out = temperature.hayes(**df, module_efficiency=0.160, + module_area=0.72, module_mass=12, surface_tilt=20, + module_emissivity=0.84) + assert_series_equal(out, expected) @pytest.mark.parametrize('tz', [None, 'Etc/GMT+5']) From bab8aa0a1bbb8a1fabc8f2580d988e41381e4da4 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 28 Sep 2022 17:34:58 -0400 Subject: [PATCH 20/20] whatsnew --- docs/sphinx/source/whatsnew/v0.9.4.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.9.4.rst b/docs/sphinx/source/whatsnew/v0.9.4.rst index c95502bae1..7afaa3d634 100644 --- a/docs/sphinx/source/whatsnew/v0.9.4.rst +++ b/docs/sphinx/source/whatsnew/v0.9.4.rst @@ -9,8 +9,11 @@ Deprecations Enhancements ~~~~~~~~~~~~ +* Added :py:func:`pvlib.temperature.hayes`, a transient cell temperature model + for fixed-tilt CdTe systems. (:issue:`1080`, :pull:`1083`) * Multiple code style issues fixed that were reported by LGTM analysis. (:issue:`1275`, :pull:`1559`) + Bug fixes ~~~~~~~~~ @@ -34,4 +37,5 @@ Requirements Contributors ~~~~~~~~~~~~ -* Christian Orner (:ghuser:`chrisorner`) \ No newline at end of file +* Christian Orner (:ghuser:`chrisorner`) +* Stephen Kaplan (:ghuser:`stephenjkaplan`)