Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
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 @@ -42,6 +42,7 @@ PV temperature models
temperature.sapm_cell_from_module
temperature.pvsyst_cell
temperature.faiman
temperature.faiman_rad
temperature.fuentes
temperature.ross
temperature.noct_sam
Expand Down
9 changes: 6 additions & 3 deletions docs/sphinx/source/whatsnew/v0.9.4.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ Enhancements
* Added a function to calculate one of GHI, DHI, and DNI from values of the other two.
:py:func:`~pvlib.irradiance.complete_irradiance`
(:issue:`1565`, :pull:`1567`)
* Add optional ``return_components`` parameter to :py:func:`pvlib.irradiance.haydavies` to return
* Added optional ``return_components`` parameter to :py:func:`pvlib.irradiance.haydavies` to return
individual diffuse irradiance components (:issue:`1553`, :pull:`1568`)

* Added a module temperature model that accounts for radiative losses to the sky
in a simplified way, using the Faiman model as an example.
:py:func:`~pvlib.temperature.faiman_rad`
(:issue:`1594`, :pull:`1595`)

Bug fixes
~~~~~~~~~
Expand All @@ -40,7 +43,6 @@ Benchmarking
~~~~~~~~~~~~~
* Removed ``time_tracker_singleaxis`` function from tracking.py (:issue:`1508`, :pull:`1535`)


Requirements
~~~~~~~~~~~~

Expand All @@ -57,4 +59,5 @@ Contributors
* Kevin Anderson (:ghuser:`kanderso-nrel`)
* Karel De Brabandere (:ghuser:`kdebrab`)
* Naman Priyadarshi (:ghuser:`Naman-Priyadarshi`)
* Adam R. Jensen (:ghuser:`AdamRJensen`)
* Echedey Luis (:ghuser:`echedey-ls`)
132 changes: 127 additions & 5 deletions pvlib/temperature.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from pvlib._deprecation import warn_deprecated
from pvlib.tools import _get_sample_intervals
import scipy
import scipy.constants
import warnings


Expand Down Expand Up @@ -318,7 +319,7 @@ def pvsyst_cell(poa_global, temp_air, wind_speed=1.0, u_c=29.0, u_v=0.0,
u_v : float, default 0.0
Combined heat loss factor influenced by wind. Parameter :math:`U_{v}`
in :eq:`pvsyst`.
:math:`\left[ \frac{\text{W}/\text{m}^2}{\text{C}\ \left( \text{m/s} \right)} \right]` # noQA: E501
:math:`\left[ \frac{\text{W}/\text{m}^2}{\text{C}\ \left( \text{m/s} \right)} \right]`

eta_m : numeric, default None (deprecated, use module_efficiency instead)

Expand Down Expand Up @@ -375,7 +376,7 @@ def pvsyst_cell(poa_global, temp_air, wind_speed=1.0, u_c=29.0, u_v=0.0,
>>> params = TEMPERATURE_MODEL_PARAMETERS['pvsyst']['freestanding']
>>> pvsyst_cell(1000, 10, **params)
37.93103448275862
"""
""" # noQA: E501

if eta_m:
warn_deprecated(
Expand Down Expand Up @@ -413,12 +414,14 @@ def faiman(poa_global, temp_air, wind_speed=1.0, u0=25.0, u1=6.84):

u0 : numeric, default 25.0
Combined heat loss factor coefficient. The default value is one
determined by Faiman for 7 silicon modules.
determined by Faiman for 7 silicon modules
in the Negev desert on an open rack at 30.9° tilt.
:math:`\left[\frac{\text{W}/{\text{m}^2}}{\text{C}}\right]`

u1 : numeric, default 6.84
Combined heat loss factor influenced by wind. The default value is one
determined by Faiman for 7 silicon modules.
determined by Faiman for 7 silicon modules
in the Negev desert on an open rack at 30.9° tilt.
:math:`\left[ \frac{\text{W}/\text{m}^2}{\text{C}\ \left( \text{m/s} \right)} \right]`

Returns
Expand All @@ -434,6 +437,7 @@ def faiman(poa_global, temp_air, wind_speed=1.0, u0=25.0, u1=6.84):
----------
.. [1] Faiman, D. (2008). "Assessing the outdoor operating temperature of
photovoltaic modules." Progress in Photovoltaics 16(4): 307-315.
:doi:`10.1002/pip.813`

.. [2] "IEC 61853-2 Photovoltaic (PV) module performance testing and energy
rating - Part 2: Spectral responsivity, incidence angle and module
Expand All @@ -442,7 +446,12 @@ def faiman(poa_global, temp_air, wind_speed=1.0, u0=25.0, u1=6.84):
.. [3] "IEC 61853-3 Photovoltaic (PV) module performance testing and energy
rating - Part 3: Energy rating of PV modules". IEC, Geneva, 2018.

'''
See also
--------
pvlib.temperature.faiman_rad

''' # noQA: E501

# Contributed by Anton Driesse (@adriesse), PV Performance Labs. Dec., 2019

# The following lines may seem odd since u0 & u1 are probably scalar,
Expand All @@ -457,6 +466,119 @@ def faiman(poa_global, temp_air, wind_speed=1.0, u0=25.0, u1=6.84):
return temp_air + temp_difference


def faiman_rad(poa_global, temp_air, wind_speed=1.0, ir_down=None,
u0=25.0, u1=6.84, sky_view=1.0, emissivity=0.88):
r'''
Calculate cell or module temperature using the Faiman model augmented
with a radiative loss term.

The Faiman model uses an empirical heat loss factor model [1]_ and is
adopted in the IEC 61853 standards [2]_ and [3]_. The radiative loss
term was proposed and developed by Driesse [4]_.

The model can be used to represent cell or module temperature.

Parameters
----------
poa_global : numeric
Total incident irradiance [W/m^2].

temp_air : numeric
Ambient dry bulb temperature [C].

wind_speed : numeric, default 1.0
Wind speed measured at the same height for which the wind loss
factor was determined. The default value 1.0 m/s is the wind
speed at module height used to determine NOCT. [m/s]

ir_down : numeric, default 0.0
Downwelling infrared radiation from the sky, measured on a horizontal
surface. [W/m^2]

u0 : numeric, default 25.0
Combined heat loss factor coefficient. The default value is one
determined by Faiman for 7 silicon modules
in the Negev desert on an open rack at 30.9° tilt.
:math:`\left[\frac{\text{W}/{\text{m}^2}}{\text{C}}\right]`

u1 : numeric, default 6.84
Combined heat loss factor influenced by wind. The default value is one
determined by Faiman for 7 silicon modules
in the Negev desert on an open rack at 30.9° tilt.
:math:`\left[ \frac{\text{W}/\text{m}^2}{\text{C}\ \left( \text{m/s} \right)} \right]`

sky_view : numeric, default 1.0
Effective view factor limiting the radiative exchange between the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is "effective" adding something here? sky_view looks to be the fraction of the sky dome "seen" by the module, so that's the usual view factor. I wonder if the formula could be considered part of the model and the input would be surface_tilt, but I'll defer to the model author.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I snuck in the "effective" because the infra-red sky is very non-isotropic and that needs to be taken into account. The formula is really just a starting point for future work, not adequately explored and validated.

module and the sky. For a tilted array the expressions
(1 + 3*cos(tilt)) / 4 can be used as a first estimate for sky_view
as discussed in [4]_. The default value is for a horizontal module.
[unitless]

emissivity : numeric, default 0.88
Infrared emissivity of the module surface facing the sky. The default
value represents the middle of a range of values found in the
literature. [unitless]

Returns
-------
numeric, values in degrees Celsius

Notes
-----
All arguments may be scalars or vectors. If multiple arguments
are vectors they must be the same length.

When only irradiance, air temperature and wind speed inputs are provided
(`ir_down` is `None`) this function calculates the same device temperature
as the original faiman model. When down-welling long-wave radiation data
are provided as well (`ir_down` is not None) the default u0 and u1 values
from the original model should not be used because a portion of the
radiative losses would be double-counted.

References
----------
.. [1] Faiman, D. (2008). "Assessing the outdoor operating temperature of
photovoltaic modules." Progress in Photovoltaics 16(4): 307-315.
:doi:`10.1002/pip.813`

.. [2] "IEC 61853-2 Photovoltaic (PV) module performance testing and energy
rating - Part 2: Spectral responsivity, incidence angle and module
operating temperature measurements". IEC, Geneva, 2018.

.. [3] "IEC 61853-3 Photovoltaic (PV) module performance testing and energy
rating - Part 3: Energy rating of PV modules". IEC, Geneva, 2018.

.. [4] Driesse, A. et al (2022) "Improving Common PV Module Temperature
Models by Incorporating Radiative Losses to the Sky". SAND2022-11604.
:doi:`10.2172/1884890`

See also
--------
pvlib.temperature.faiman

''' # noQA: E501

# Contributed by Anton Driesse (@adriesse), PV Performance Labs. Nov., 2022

u0 = np.asanyarray(u0)
u1 = np.asanyarray(u1)
emissivity = np.asanyarray(emissivity)

abs_zero = np.array(-273.15)
sigma = np.array(scipy.constants.Stefan_Boltzmann)

if ir_down is None:
qrad_sky = np.array(0.0)
Copy link
Member

@AdamRJensen AdamRJensen Dec 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
u0 = np.asanyarray(u0)
u1 = np.asanyarray(u1)
emissivity = np.asanyarray(emissivity)
abs_zero = np.array(-273.15)
sigma = np.array(scipy.constants.Stefan_Boltzmann)
if ir_down is None:
qrad_sky = np.array(0.0)
poa_global = np.asanyarray(poa_global)
temp_air = np.asanyarray(temp_air)
wind_speed = np.asanyarray(wind_speed)
ir_down = np.asanyarray(ir_down)
u0 = np.asanyarray(u0)
u1 = np.asanyarray(u1)
sky_view = np.asanyarray(sky_view)
emissivity = np.asanyarray(emissivity)
abs_zero = -273.15
sigma = scipy.constants.Stefan_Boltzmann
if ir_down is None:
qrad_sky = 0.0

I see that the usage of np.asanyarray is very carefully chosen such that all inputs can be passed as lists and directly or indirectly be converted to arrays. The indirect conversion to arrays (by multiplying a list something array-like) seems undesirable and unintuitive to the user. Why not instead just convert all the inputs to array-like in the beginning and be straightforward about it?

It's not a blocker for me, but this is the point that I was trying to raise earlier.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the whole I believe this function behaves like numpy would. The reason for the indirect and partial approach is to allow pandas and xarray data structures to be propagated from the first four arguments. Try it, you might like it!

Copy link
Member

@wholmgren wholmgren Dec 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about this function requires any of the np.asanyarray calls? np.asanyarray is not robust.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, now I see the hidden conversation about allowing list input. That is not needed and comes at a cost of disallowing arbitrary array-like input that doesn't fully support the asanyarray machinery (e.g. dask, or whatever the pydata library of the month is).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate on what doesn't work here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall I'm getting more push-back than expected on something which I though was a very simple technique to make the function very flexible. If there is a concrete down-side I'd like to learn about it, especially the risks associated with asanyarray (which could be replaced with asarray in this particular function). I've use this technique before and am likely to use it again unless there really is a problem with it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function is already very flexible because you did a good job writing it :). It doesn't need asanyarray! Here's a concrete downside for asanyarray:

In [1]: import numpy as np

In [2]: import dask.array as da

In [3]: da.from_array([0, 1])
Out[3]: dask.array<array, shape=(2,), dtype=int32, chunksize=(2,), chunktype=numpy.ndarray>

In [4]: np.asanyarray(da.from_array([0, 1]))
Out[4]: array([0, 1])

In [5]: np.asarray(da.from_array([0, 1]))
Out[5]: array([0, 1])

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

np.asanyarray is not robust.

I am curious to learn more about this...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Anton, here's a quote from NEP16

Sometimes people suggest using np.asanyarray for this purpose, but unfortunately its semantics are exactly backwards: it guarantees that the object it returns uses the same memory layout as an ndarray, but tells you nothing at all about its semantics, which makes it essentially impossible to use safely in practice.

The scientific python brain trust has discussed solutions in a variety of venues and I find it very difficult to track, but here's a very incomplete list of links:

I think we have another issue or two related to this, so probably best to continue discussion there rather than a closed PR :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! That was educational!

else:
ir_up = sigma * ((temp_air - abs_zero)**4)
qrad_sky = emissivity * sky_view * (ir_up - ir_down)

heat_input = poa_global - qrad_sky
total_loss_factor = u0 + u1 * wind_speed
temp_difference = heat_input / total_loss_factor
return temp_air + temp_difference


def ross(poa_global, temp_air, noct):
r'''
Calculate cell temperature using the Ross model.
Expand Down
33 changes: 29 additions & 4 deletions pvlib/tests/test_temperature.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,12 @@ def test_pvsyst_cell_eta_m_deprecated():

def test_faiman_default():
result = temperature.faiman(900, 20, 5)
assert_allclose(result, 35.203, 0.001)
assert_allclose(result, 35.203, atol=0.001)


def test_faiman_kwargs():
result = temperature.faiman(900, 20, wind_speed=5.0, u0=22.0, u1=6.)
assert_allclose(result, 37.308, 0.001)
assert_allclose(result, 37.308, atol=0.001)


def test_faiman_list():
Expand All @@ -122,7 +122,7 @@ def test_faiman_list():
winds = [10, 5, 0]
result = temperature.faiman(irrads, temps, wind_speed=winds)
expected = np.array([0.0, 18.446, 5.0])
assert_allclose(expected, result, 3)
assert_allclose(expected, result, atol=0.001)


def test_faiman_ndarray():
Expand All @@ -131,7 +131,32 @@ def test_faiman_ndarray():
winds = np.array([10, 5, 0])
result = temperature.faiman(irrads, temps, wind_speed=winds)
expected = np.array([0.0, 18.446, 5.0])
assert_allclose(expected, result, 3)
assert_allclose(expected, result, atol=0.001)


def test_faiman_rad_no_ir():
expected = temperature.faiman(900, 20, 5)
result = temperature.faiman_rad(900, 20, 5)
assert_allclose(result, expected)


def test_faiman_rad_ir():
ir_down = [0, 100, 200, 315.6574, 400]
expected = [-11.111, -7.591, -4.071, -0.000, 2.969]
result = temperature.faiman_rad(0, 0, 0, ir_down)
assert_allclose(result, expected, atol=0.001)

sky_view = [1.0, 0.5, 0.0]
expected = [-4.071, -2.036, 0.000]
result = temperature.faiman_rad(0, 0, 0, ir_down=200,
sky_view=sky_view)
assert_allclose(result, expected, atol=0.001)

emissivity = [1.0, 0.88, 0.5, 0.0]
expected = [-4.626, -4.071, -2.313, 0.000]
result = temperature.faiman_rad(0, 0, 0, ir_down=200,
emissivity=emissivity)
assert_allclose(result, expected, atol=0.001)


def test_ross():
Expand Down