Skip to content
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

Draft
wants to merge 22 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0e96b63
checkpoint. initial implementation of hayes model. still need to calc…
stephenjkaplan Oct 16, 2020
364bbd4
committing for initial pull request. still need to write tests, docum…
stephenjkaplan Oct 16, 2020
2c560d7
updates entries to docs/sphinx/source/api.rst and wrote clear first l…
stephenjkaplan Oct 17, 2020
0179194
fixed complaints from Stickler CI (mostly line length)
stephenjkaplan Oct 17, 2020
381249a
initial drat of documented functions
stephenjkaplan Oct 17, 2020
f8927ff
initial drat of documented functions
stephenjkaplan Oct 17, 2020
f39c338
fixed documentation
stephenjkaplan Oct 17, 2020
bfdcf0e
placeholder unit tests, some documentation fixes.
stephenjkaplan Oct 17, 2020
840beb7
added main placeholder test and temporarily took out module area in s…
stephenjkaplan Oct 19, 2020
b4be142
made fix for SticklerCI
stephenjkaplan Oct 19, 2020
2399607
fixed unit test
stephenjkaplan Oct 19, 2020
3138b0f
Merge remote-tracking branch 'upstream/master' into pr/1083
kandersolar Jun 24, 2021
1e138c0
cleanup
kandersolar Jun 24, 2021
a41337e
remove obsolete api.rst entry
kandersolar Sep 28, 2022
f382478
Merge remote-tracking branch 'upstream/master' into pr/1083
kandersolar Sep 28, 2022
5203233
add to new api listing
kandersolar Sep 28, 2022
ee2388d
some cleanup
kandersolar Sep 28, 2022
d2ad4e1
more cleanup
kandersolar Sep 28, 2022
41da4c9
more cleanup
kandersolar Sep 28, 2022
4ea7671
fix pesky integer truncation bug
kandersolar Sep 28, 2022
db57abb
a couple very basic tests
kandersolar Sep 28, 2022
bab8aa0
whatsnew
kandersolar Sep 28, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/sphinx/source/reference/pv_modeling.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ PV temperature models
temperature.pvsyst_cell
temperature.faiman
temperature.fuentes
temperature.hayes
temperature.ross
temperature.noct_sam
temperature.prilliman
Expand Down
6 changes: 5 additions & 1 deletion docs/sphinx/source/whatsnew/v0.9.4.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~

Expand All @@ -34,4 +37,5 @@ Requirements

Contributors
~~~~~~~~~~~~
* Christian Orner (:ghuser:`chrisorner`)
* Christian Orner (:ghuser:`chrisorner`)
* Stephen Kaplan (:ghuser:`stephenjkaplan`)
227 changes: 226 additions & 1 deletion pvlib/temperature.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import numpy as np
import pandas as pd
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
Expand Down Expand Up @@ -720,6 +720,231 @@ 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, emissivity1,
emissivity2, 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]

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
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 * \
(emissivity1 * temperature1 ** 4 - emissivity2 * temperature2 ** 4)

return q


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):
"""
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. 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_global : 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_mass : float
Mass of PV module [kg]

surface_tilt : float
Tilt angle of fixed tilt array [deg]

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.

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.

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
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
[1]_ to minimize error.

Returns
-------
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
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`.
"""
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_global

# converted electrical energy
p_out = module_efficiency * q_short_wave_radiation

# adjust wind speed if sensor height not at 2.5 meters
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

# sky temperature assumed to be constant offset from ambient (see Table 1)
t_sky = temp_air - 20

# calculate view factors (simplified calculations)
# 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. 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

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
for i in range(len(t_mod) - 1):
# calculate long wave radiation (radiative interactions between module
# and objects within Earth's atmosphere)
q_mod_sky = _calculate_radiative_heat(
module_area=module_area,
view_factor=view_factor_mod_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=module_emissivity,
emissivity2=ground_emissivity,
temperature1=t_mod_i,
temperature2=t_mod_i
)
q_mod_mod = 0 # current assumption is that it is negligible
# 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]
)
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_global.index, name='tmod')


def _adj_for_mounting_standoff(x):
# supports noct cell temperature function. Except for x > 3.5, the SAM code
# and documentation aren't clear on the precise intervals. The choice of
Expand Down
33 changes: 32 additions & 1 deletion pvlib/tests/test_temperature.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from .conftest import DATA_DIR, assert_series_equal
from numpy.testing import assert_allclose

from pvlib import temperature, tools
from pvlib import temperature, location, irradiance, iam, tools
from pvlib._deprecation import pvlibDeprecationWarning

import re
Expand Down Expand Up @@ -210,6 +210,37 @@ def test_fuentes(filename, inoct):
assert night_difference.min() > 0


@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'])
def test_fuentes_timezone(tz):
index = pd.date_range('2019-01-01', freq='h', periods=3, tz=tz)
Expand Down