Skip to content

Change units on SAPM effective irradiance from suns to W/m2 #815

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

Merged
merged 14 commits into from
Nov 26, 2019
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 10 additions & 2 deletions docs/sphinx/source/whatsnew/v0.7.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,16 @@ compatibility notes.
**Python 2.7 support ended on June 1, 2019.** (:issue:`501`)
**Minimum numpy version is now 1.10.4. Minimum pandas version is now 0.18.1.**

API Changes
~~~~~~~~~~~
API Breaking Changes
~~~~~~~~~~~~~~~~~~~~
* The `effective_irradiance` argument for :py:func:`pvsystem.sapm` now requires
units of W/m^2. Previously, units for this input were suns. A RuntimeWarning
warning is raised if all `effective_irradiance < 2.0`.
* The output of :py:func:`pvsystem.sapm_effective_irradiance` is now in units
of W/m2 rather than suns.

API Changes with Deprecations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Changes related to cell temperature models (:issue:`678`):
* Changes to functions
- Moved functions for cell temperature from `pvsystem.py` to `temperature.py`.
Expand Down
2 changes: 1 addition & 1 deletion pvlib/modelchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ def infer_dc_model(self):
'set the model with the dc_model kwarg.')

def sapm(self):
self.dc = self.system.sapm(self.effective_irradiance/1000.,
self.dc = self.system.sapm(self.effective_irradiance,
self.cell_temperature)

self.dc = self.system.scale_voltage_current_power(self.dc)
Expand Down
100 changes: 67 additions & 33 deletions pvlib/pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,28 +563,25 @@ def sapm_effective_irradiance(self, poa_direct, poa_diffuse,
Parameters
----------
poa_direct : numeric
The direct irradiance incident upon the module.
The direct irradiance incident upon the module. [W/m2]

poa_diffuse : numeric
The diffuse irradiance incident on module.
The diffuse irradiance incident on module. [W/m2]

airmass_absolute : numeric
Absolute airmass.
Absolute airmass. [unitless]

aoi : numeric
Angle of incidence in degrees.

reference_irradiance : numeric, default 1000
Reference irradiance by which to divide the input irradiance.
Angle of incidence. [degrees]

Returns
-------
effective_irradiance : numeric
The SAPM effective irradiance.
The SAPM effective irradiance. [W/m2]
"""
return sapm_effective_irradiance(
poa_direct, poa_diffuse, airmass_absolute, aoi,
self.module_parameters, reference_irradiance=reference_irradiance)
self.module_parameters)

def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0):
"""Uses :py:func:`temperature.pvsyst_cell` to calculate cell
Expand Down Expand Up @@ -1580,10 +1577,11 @@ def sapm(effective_irradiance, temp_cell, module):
Parameters
----------
effective_irradiance : numeric
Effective irradiance (suns).
Irradiance reaching the module's cells, after reflections and
adjustment for spectrum. [W/m2]

temp_cell : numeric
The cell temperature (degrees C).
Cell temperature [C].

module : dict-like
A dict or Series defining the SAPM parameters. See the notes section
Expand Down Expand Up @@ -1659,12 +1657,23 @@ def sapm(effective_irradiance, temp_cell, module):
temperature.sapm_module
'''

T0 = 25
# TODO: someday, change temp_ref and irrad_ref to reference_temperature and
# reference_irradiance and expose
temp_ref = 25
irrad_ref = 1000
# TODO: remove this warning in v0.8 after deprecation period for change in
# effective irradiance units, made in v0.7
if np.all(effective_irradiance) < 2.0:
import warnings
warnings.warn('effective_irradiance inputs appear to be in suns.'
' Units changed in v0.7 from suns to W/m2',
RuntimeWarning)

q = 1.60218e-19 # Elementary charge in units of coulombs
kb = 1.38066e-23 # Boltzmann's constant in units of J/K

# avoid problem with integer input
Ee = np.array(effective_irradiance, dtype='float64')
Ee = np.array(effective_irradiance, dtype='float64') / irrad_ref

# set up masking for 0, positive, and nan inputs
Ee_gt_0 = np.full_like(Ee, False, dtype='bool')
Expand All @@ -1687,32 +1696,32 @@ def sapm(effective_irradiance, temp_cell, module):
out = OrderedDict()

out['i_sc'] = (
module['Isco'] * Ee * (1 + module['Aisc']*(temp_cell - T0)))
module['Isco'] * Ee * (1 + module['Aisc']*(temp_cell - temp_ref)))

out['i_mp'] = (
module['Impo'] * (module['C0']*Ee + module['C1']*(Ee**2)) *
(1 + module['Aimp']*(temp_cell - T0)))
(1 + module['Aimp']*(temp_cell - temp_ref)))

out['v_oc'] = np.maximum(0, (
module['Voco'] + cells_in_series * delta * logEe +
Bvoco*(temp_cell - T0)))
Bvoco*(temp_cell - temp_ref)))

out['v_mp'] = np.maximum(0, (
module['Vmpo'] +
module['C2'] * cells_in_series * delta * logEe +
module['C3'] * cells_in_series * ((delta * logEe) ** 2) +
Bvmpo*(temp_cell - T0)))
Bvmpo*(temp_cell - temp_ref)))

out['p_mp'] = out['i_mp'] * out['v_mp']

out['i_x'] = (
module['IXO'] * (module['C4']*Ee + module['C5']*(Ee**2)) *
(1 + module['Aisc']*(temp_cell - T0)))
(1 + module['Aisc']*(temp_cell - temp_ref)))

# the Ixx calculation in King 2004 has a typo (mixes up Aisc and Aimp)
out['i_xx'] = (
module['IXXO'] * (module['C6']*Ee + module['C7']*(Ee**2)) *
(1 + module['Aisc']*(temp_cell - T0)))
(1 + module['Aisc']*(temp_cell - temp_ref)))

if isinstance(out['i_sc'], pd.Series):
out = pd.DataFrame(out)
Expand Down Expand Up @@ -1839,45 +1848,70 @@ def sapm_spectral_loss(airmass_absolute, module):


def sapm_effective_irradiance(poa_direct, poa_diffuse, airmass_absolute, aoi,
module, reference_irradiance=1000):
"""
module):
r"""
Calculates the SAPM effective irradiance using the SAPM spectral
loss and SAPM angle of incidence loss functions.

Parameters
----------
poa_direct : numeric
The direct irradiance incident upon the module.
The direct irradiance incident upon the module. [W/m2]

poa_diffuse : numeric
The diffuse irradiance incident on module.
The diffuse irradiance incident on module. [W/m2]

airmass_absolute : numeric
Absolute airmass.
Absolute airmass. [unitless]

aoi : numeric
Angle of incidence in degrees.
Angle of incidence. [degrees]

module : dict-like
A dict, Series, or DataFrame defining the SAPM performance
parameters. See the :py:func:`sapm` notes section for more
details.

reference_irradiance : numeric, default 1000
Copy link
Member

Choose a reason for hiding this comment

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

removed doc string but did not remove from function signature.

Copy link
Member

Choose a reason for hiding this comment

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

what's the logic behind removing the kwarg from this function and adding it to the other? I suggest we should consistently provide flexibility or consistently enforce W/m^2.

Copy link
Member Author

Choose a reason for hiding this comment

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

Removed from signature (was my oversight) to be consistent with the input expected by other irradiance-to-DC power functions, i.e., effective irradiance in W/m2. SAPM was the oddball.

Reference irradiance by which to divide the input irradiance.

Returns
-------
effective_irradiance : numeric
The SAPM effective irradiance.
Effective irradiance accounting for reflections and spectral content.
[W/m2]

Notes
-----
The SAPM model for effective irradiance [1] translates broadband direct and
diffuse irradiance on the plane of array to the irradiance absorbed by a
module's cells.

The model is
.. math::

`Ee = f_1(AM_a) (E_b f_2(AOI) + f_d E_d)`

where :math:`Ee` is effective irradiance (W/m2), :math:`f_1` is a fourth
degree polynomial in air mass :math:`AM_a`, :math:`E_b` is beam (direct)
irradiance on the plane of array, :math:`E_d` is diffuse irradiance on the
plane of array, :math:`f_2` is a fifth degree polynomial in the angle of
incidence :math:`AOI`, and :math:`f_d` is the fraction of diffuse
irradiance on the plane of array that is not reflected away.

References
----------
[1] D. King et al, "Sandia Photovoltaic Array Performance Model",
SAND2004-3535, Sandia National Laboratories, Albuquerque, NM

See also
--------
pvlib.iam.sapm
pvlib.pvsystem.sapm_spectral_loss
pvlib.pvsystem.sapm
"""

F1 = sapm_spectral_loss(airmass_absolute, module)
F2 = iam.sapm(aoi, module)

E0 = reference_irradiance

Ee = F1 * (poa_direct*F2 + module['FD']*poa_diffuse) / E0
Ee = F1 * (poa_direct * F2 + module['FD'] * poa_diffuse)

return Ee

Expand Down Expand Up @@ -1992,7 +2026,7 @@ def singlediode(photocurrent, saturation_current, resistance_series,
the IV curve are linearly spaced.

References
-----------
----------
[1] S.R. Wenham, M.A. Green, M.E. Watt, "Applied Photovoltaics" ISBN
0 86758 909 4

Expand Down
59 changes: 32 additions & 27 deletions pvlib/test/test_pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,8 @@ def test_retrieve_sam_cecinverter():
def test_sapm(sapm_module_params):

times = pd.date_range(start='2015-01-01', periods=5, freq='12H')
effective_irradiance = pd.Series([-1, 0.5, 1.1, np.nan, 1], index=times)
effective_irradiance = pd.Series([-1000, 500, 1100, np.nan, 1000],
index=times)
temp_cell = pd.Series([10, 25, 50, 25, np.nan], index=times)

out = pvsystem.sapm(effective_irradiance, temp_cell, sapm_module_params)
Expand All @@ -216,7 +217,7 @@ def test_sapm(sapm_module_params):

assert_frame_equal(out, expected, check_less_precise=4)

out = pvsystem.sapm(1, 25, sapm_module_params)
out = pvsystem.sapm(1000, 25, sapm_module_params)

expected = OrderedDict()
expected['i_sc'] = 5.09115
Expand All @@ -235,10 +236,21 @@ def test_sapm(sapm_module_params):
pd.Series(sapm_module_params))


def test_pvsystem_sapm_warning(sapm_module_params):
# deprecation warning for change in effective_irradiance units in
# pvsystem.sapm
# TODO: remove after deprecation period (v0.8)
effective_irradiance = np.array([0.1, 0.2, 1.3])
temp_cell = np.array([25, 25, 50])
warn_txt = 'effective_irradiance inputs appear to be in suns'
with pytest.warns(RuntimeWarning, match=warn_txt):
pvsystem.sapm(effective_irradiance, temp_cell, sapm_module_params)


def test_PVSystem_sapm(sapm_module_params, mocker):
mocker.spy(pvsystem, 'sapm')
system = pvsystem.PVSystem(module_parameters=sapm_module_params)
effective_irradiance = 0.5
effective_irradiance = 500
temp_cell = 25
out = system.sapm(effective_irradiance, temp_cell)
pvsystem.sapm.assert_called_once_with(effective_irradiance, temp_cell,
Expand Down Expand Up @@ -295,33 +307,23 @@ def test_PVSystem_first_solar_spectral_loss(module_parameters, module_type,


@pytest.mark.parametrize('test_input,expected', [
([1000, 100, 5, 45, 1000], 1.1400510967821877),
([1000, 100, 5, 45], 1140.0510967821877),
([np.array([np.nan, 1000, 1000]),
np.array([100, np.nan, 100]),
np.array([1.1, 1.1, 1.1]),
np.array([10, 10, 10]),
1000],
np.array([np.nan, np.nan, 1.081157])),
np.array([10, 10, 10])],
np.array([np.nan, np.nan, 1081.1574])),
([pd.Series([1000]), pd.Series([100]), pd.Series([1.1]),
pd.Series([10]), 1370],
pd.Series([0.789166]))
pd.Series([10])],
pd.Series([1081.1574]))
])
def test_sapm_effective_irradiance(sapm_module_params, test_input, expected):

try:
kwargs = {'reference_irradiance': test_input[4]}
test_input = test_input[:-1]
except IndexError:
kwargs = {}

test_input.append(sapm_module_params)

out = pvsystem.sapm_effective_irradiance(*test_input, **kwargs)

out = pvsystem.sapm_effective_irradiance(*test_input)
if isinstance(test_input, pd.Series):
assert_series_equal(out, expected, check_less_precise=4)
else:
assert_allclose(out, expected, atol=1e-4)
assert_allclose(out, expected, atol=1e-1)


def test_PVSystem_sapm_effective_irradiance(sapm_module_params, mocker):
Expand All @@ -332,15 +334,16 @@ def test_PVSystem_sapm_effective_irradiance(sapm_module_params, mocker):
poa_diffuse = 100
airmass_absolute = 1.5
aoi = 0
reference_irradiance = 1000

p = (sapm_module_params['A4'], sapm_module_params['A3'],
sapm_module_params['A2'], sapm_module_params['A1'],
sapm_module_params['A0'])
f1 = np.polyval(p, airmass_absolute)
expected = f1 * (poa_direct + sapm_module_params['FD'] * poa_diffuse)
out = system.sapm_effective_irradiance(
poa_direct, poa_diffuse, airmass_absolute,
aoi, reference_irradiance=reference_irradiance)
poa_direct, poa_diffuse, airmass_absolute, aoi)
pvsystem.sapm_effective_irradiance.assert_called_once_with(
poa_direct, poa_diffuse, airmass_absolute, aoi, sapm_module_params,
reference_irradiance=reference_irradiance)
assert_allclose(out, 1, atol=0.1)
poa_direct, poa_diffuse, airmass_absolute, aoi, sapm_module_params)
assert_allclose(out, expected, atol=0.1)


def test_PVSystem_sapm_celltemp(mocker):
Expand Down Expand Up @@ -1464,8 +1467,10 @@ def test_PVSystem_pvwatts_ac_kwargs(mocker):

@fail_on_pvlib_version('0.8')
def test_deprecated_08():
# deprecated function pvsystem.sapm_celltemp
with pytest.warns(pvlibDeprecationWarning):
pvsystem.sapm_celltemp(1000, 25, 1)
# deprecated function pvsystem.pvsyst_celltemp
with pytest.warns(pvlibDeprecationWarning):
pvsystem.pvsyst_celltemp(1000, 25)
module_parameters = {'R_sh_ref': 1, 'a_ref': 1, 'I_o_ref': 1,
Expand Down