diff --git a/docs/sphinx/source/whatsnew/v0.7.0.rst b/docs/sphinx/source/whatsnew/v0.7.0.rst index 4bfe9fac9a..81e02135fd 100644 --- a/docs/sphinx/source/whatsnew/v0.7.0.rst +++ b/docs/sphinx/source/whatsnew/v0.7.0.rst @@ -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`. diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 46bf080deb..1ab916cd77 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -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) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 550bea0171..93491a5704 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -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 @@ -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 @@ -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') @@ -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) @@ -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 - 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 @@ -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 diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index bc9f1426a7..c9bec9529d 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -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) @@ -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 @@ -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, @@ -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): @@ -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): @@ -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,