-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Hayes temp model #1083
base: main
Are you sure you want to change the base?
Hayes temp model #1083
Conversation
…ulate long wave radiation with view factors, etc.
I pushed up this code and submitted a draft PR before full completion because I have a number of questions/bugs that need to be addressed:
I am currently testing the functionality with the following script as I work through it. I am using 5 minute PSM v3 data because the model is best suited for higher resolution inputs. I'm also assuming a fixed tilt array with 30 degree tilt, and using some module metadata from a First Solar Series 6 datasheet: from pvlib import location
from pvlib.temperature import hayes, sapm_module, TEMPERATURE_MODEL_PARAMETERS
from pvlib.iotools import read_psm3
from pvlib.irradiance import get_total_irradiance, aoi
from pvlib.iam import ashrae
import matplotlib.pyplot as plt
data = read_psm3('test_psm3_5min.csv')
module_tilt = 30
module_azimuth = 180
site = location.Location(latitude=data[0]['Latitude'], longitude=data[0]['Longitude'], tz='MST')
solar_position = site.get_solarposition(times=data[1].index)
poa_global = get_total_irradiance(
surface_tilt=module_tilt,
surface_azimuth=module_azimuth,
dni=data[1]['DNI'],
ghi=data[1]['GHI'],
dhi=data[1]['DHI'],
solar_zenith=solar_position['apparent_zenith'],
solar_azimuth=solar_position['azimuth']
)['poa_global']
temp_air = data[1]['Temperature']
wind_speed = data[1]['Wind Speed']
# 1. Calculate module temp with new model
aoi = aoi(module_tilt, module_azimuth, solar_position['zenith'], solar_position['azimuth'])
poa_effective = poa_global.multiply(ashrae(aoi))
module_efficiency = 0.176
module_area = 2.47 # m^2
module_weight = 34.5
tmod_hayes = hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, module_weight, module_tilt)
# 2. Calculate module temp with existing model for comparison
# assume glass-glass with open rack since Hayes model was validated for CdTe
temp_model_params = TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass']
tmod_sapm = sapm_module(poa_global, temp_air, wind_speed, temp_model_params['a'], temp_model_params['b'])
plt.plot(tmod_sapm[0:3000])
plt.plot(tmod_hayes[0:3000])
plt.ylim((-50, 100))
plt.legend(['SAPM', 'Hayes'])
plt.show() |
…ine in inline documentation of function for sphinx autosummary.
The temperature change equation
I didn't check that inverting the sign of |
@kanderso-nrel extremely helpful, I'll look into all of that, thanks. update: reasoned through which heat transfer would be positive and negative for the module, and the temperature trend started to look a lot more normal. converted degrees to radians also improved things. thanks again. |
Gut check against SAPM temp model as of 10/17. Definitely a lot of work to be done, although it seems like there is a relatively constant offset, and the signal noise matches somewhat closely, so that is a good sign. when I simply use POAeff for short wave radiation instead of multiplying it by module area to cancel out m^2, it becomes even closer (still with an offset) |
…hort wave heat transfer calculation
I would expect the dynamic model result to appear damped and lagged, compared with SAPM. |
@stephenjkaplan For the For the offset from SAPM -- it is to be expected at least at night because the SAPM model just sets Tcell=Tamb at night (i.e. no radiative cooling to the sky). The magnitude of the night offset looks rather larger than I'd expect, but maybe that would change if the module parameters are updated. |
Following up on the above speculation, using Series 3/4 parameters results in much better agreement with the SAPM model: Click to show source code (225 lines)import pandas as pd
import math
import numpy as np
from pandas.tseries.frequencies import to_offset
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
# TODO there are probably 2 of these values
Thermal emissivity [unitless]. Must be between 0 and 1.
temperature1 : float
Temperature of object 1 [K]
temperature2 : float
Temperature of object 2 [K]
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
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_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.
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]
module_efficiency : float
PV module efficiency [decimal]
module_area : float
Front-side surface area of PV module [m^2]
module_weight : float
Weight of PV module [kg]
module_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_sky : float, default 0.95
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. Must be between
0 and 1.
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]
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
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
# 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))
# 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
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):
# 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,
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,
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 = -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
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')
# %%
import pvlib
from pvlib import location
from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS
from pvlib.iam import ashrae
import matplotlib.pyplot as plt
headers, data = pvlib.iotools.get_psm3(40, -80, 'DEMO_KEY', '[email protected]', names='2019', interval=5)
data = data.iloc[:1500]
# %%
module_tilt = 30
module_azimuth = 180
site = location.Location(latitude=headers['Latitude'], longitude=headers['Longitude'])
solar_position = site.get_solarposition(times=data.index)
poa_global = pvlib.irradiance.get_total_irradiance(
surface_tilt=module_tilt,
surface_azimuth=module_azimuth,
dni=data['DNI'],
ghi=data['GHI'],
dhi=data['DHI'],
solar_zenith=solar_position['apparent_zenith'],
solar_azimuth=solar_position['azimuth']
)['poa_global']
temp_air = data['Temperature']
wind_speed = data['Wind Speed']
# 1. Calculate module temp with new model
aoi = pvlib.irradiance.aoi(module_tilt, module_azimuth, solar_position['zenith'], solar_position['azimuth'])
poa_effective = poa_global.multiply(ashrae(aoi))
cases = pd.DataFrame({
'FSLR Series 3': [0.72, 0.125, 12], # https://www.firstsolar.com/-/media/First-Solar/Project-Documents/PD-5-401-03_Series3Black-4_NA.ashx
'FSLR Series 4': [0.72, 0.160, 12], # https://www.firstsolar.com/-/media/First-Solar/Technical-Documents/Series-4-Datasheets/Series-4V3-Module-Datasheet.ashx
'FLSR Series 6': [2.47, 0.176, 34.5], # https://www.firstsolar.com/-/media/First-Solar/Technical-Documents/Series-6-Datasheets/Series-6-Datasheet.ashx
}, index=['module_area', 'module_efficiency', 'module_weight']).T
for name, params in cases.iterrows():
tmod_hayes = hayes(poa_effective, temp_air, wind_speed, module_tilt=module_tilt, **params)
tmod_hayes.plot(label=name)
# 2. Calculate module temp with existing model for comparison
# assume glass-glass with open rack since Hayes model was validated for CdTe
temp_model_params = TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass']
tmod_sapm = pvlib.temperature.sapm_module(poa_global, temp_air, wind_speed, temp_model_params['a'], temp_model_params['b'])
tmod_sapm.plot(label='SAPM')
plt.legend() Note that I used @stephenjkaplan do you plan on picking this PR up again? If not, and other maintainers are willing to review, I'll volunteer to finish it. |
OK I merged master into this branch and did a bit of cleanup. Still need to write tests. @stephenjkaplan I hope you don't mind me pushing to your branch for continuity -- feel free to revert my commits if you want to take over again, otherwise I'll continue on. |
@kanderso-nrel please continue on! I haven't been able to revisit this so I'm happy to see someone follow through. |
I think so. I haven't done it and don't think anyone else at Sandia has. In #717 there will be functions for computing view factors. I haven't factored them into |
FWIW here's a quick comparison with measured 1-minute Tmod data from the NIST ground array, plus the other transient models, showing that this Hayes implementation is right in there with the other models and the actual measurements: Disclaimer: the measurements aren't from a FSLR system, I used all default parameters for the models, yadda yadda.
I wonder if someone at Sandia could be convinced to do it? I suppose I could download octave and make one myself, but my MATLAB knowledge is seriously rusty. It also seems less useful as a validation if the two implementations are from one person. |
Let me see what can be done. |
docs/sphinx/source/api.rst
for API changes.docs/sphinx/source/whatsnew
for all changes. Includes link to the GitHub Issue with:issue:`num`
or this Pull Request with:pull:`num`
. Includes contributor name and/or GitHub username (link with:ghuser:`user`
). Note from Stephen: waiting on feature to be added to a particular release