diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 8237e3908a..e13eff32e2 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -524,6 +524,7 @@ ModelChain model definitions. modelchain.ModelChain.no_spectral_loss modelchain.ModelChain.sapm_temp modelchain.ModelChain.pvsyst_temp + modelchain.ModelChain.faiman_temp modelchain.ModelChain.pvwatts_losses modelchain.ModelChain.no_extra_losses diff --git a/docs/sphinx/source/whatsnew/v0.7.2.rst b/docs/sphinx/source/whatsnew/v0.7.2.rst index 999436bdbd..8d73b7f37f 100644 --- a/docs/sphinx/source/whatsnew/v0.7.2.rst +++ b/docs/sphinx/source/whatsnew/v0.7.2.rst @@ -13,6 +13,9 @@ Enhancements * TMY3 dataframe returned by :py:func:`~pvlib.iotools.read_tmy3` now contains the original ``Date (MM/DD/YYYY)`` and ``Time (HH:MM)`` columns that the indices were parsed from (:pull:`866`) +* Add :py:func:`~pvlib.pvsystem.PVSystem.faiman` and added + ``temperature_model='faiman'`` option to :py:class:`~pvlib.modelchain.ModelChain` + (:pull:`897`) (:issue:`836`). * Add Kimber soiling model :py:func:`pvlib.losses.soiling_kimber` (:pull:`860`) Bug fixes diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 45a9f5ca98..dea4cc0ac3 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -282,8 +282,9 @@ class ModelChain(object): as the first argument to a user-defined function. temperature_model: None, str or function, default None - Valid strings are 'sapm' and 'pvsyst'. The ModelChain instance will be - passed as the first argument to a user-defined function. + Valid strings are 'sapm', 'pvsyst', and 'faiman'. The ModelChain + instance will be passed as the first argument to a user-defined + function. losses_model: str or function, default 'no_loss' Valid strings are 'pvwatts', 'no_loss'. The ModelChain instance @@ -660,6 +661,8 @@ def temperature_model(self, model): self._temperature_model = self.sapm_temp elif model == 'pvsyst': self._temperature_model = self.pvsyst_temp + elif model == 'faiman': + self._temperature_model = self.faiman_temp else: raise ValueError(model + ' is not a valid temperature model') # check system.temperature_model_parameters for consistency @@ -679,6 +682,8 @@ def infer_temperature_model(self): return self.sapm_temp elif set(['u_c', 'u_v']) <= params: return self.pvsyst_temp + elif set(['u0', 'u1']) <= params: + return self.faiman_temp else: raise ValueError('could not infer temperature model from ' 'system.temperature_module_parameters {}.' @@ -696,6 +701,12 @@ def pvsyst_temp(self): self.weather['wind_speed']) return self + def faiman_temp(self): + self.cell_temperature = self.system.faiman_celltemp( + self.total_irrad['poa_global'], self.weather['temp_air'], + self.weather['wind_speed']) + return self + @property def losses_model(self): return self._losses_model diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index d9a24b9b52..8b5f868321 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -600,13 +600,6 @@ def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): loss factor was determined. The default value is 1.0, which is the wind speed at module height used to determine NOCT. - eta_m : numeric, default 0.1 - Module external efficiency as a fraction, i.e., - DC power / poa_global. - - alpha_absorption : numeric, default 0.9 - Absorption coefficient - Returns ------- numeric, values in degrees C. @@ -618,6 +611,32 @@ def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): return temperature.pvsyst_cell(poa_global, temp_air, wind_speed, **kwargs) + def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): + """ + Use :py:func:`temperature.faiman` to calculate cell 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 in m/s 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] + + Returns + ------- + numeric, values in degrees C. + """ + kwargs = _build_kwargs(['u0', 'u1'], + self.temperature_model_parameters) + return temperature.faiman(poa_global, temp_air, wind_speed, + **kwargs) + def first_solar_spectral_loss(self, pw, airmass_absolute): """ diff --git a/pvlib/temperature.py b/pvlib/temperature.py index fed5cdca89..6aceba1532 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -274,8 +274,8 @@ def pvsyst_cell(poa_global, temp_air, wind_speed=1.0, u_c=29.0, u_v=0.0, def faiman(poa_global, temp_air, wind_speed=1.0, u0=25.0, u1=6.84): ''' Calculate cell or module temperature using an empirical heat loss factor - model as proposed by Faiman [1] and adopted in the IEC 61853 - standards [2] and [3]. + model as proposed by Faiman [1]_ and adopted in the IEC 61853 + standards [2]_ and [3]_. Usage of this model in the IEC 61853 standard does not distinguish between cell and module temperature. @@ -312,15 +312,15 @@ def faiman(poa_global, temp_air, wind_speed=1.0, u0=25.0, u1=6.84): References ---------- - [1] Faiman, D. (2008). "Assessing the outdoor operating temperature of - photovoltaic modules." Progress in Photovoltaics 16(4): 307-315. + .. [1] Faiman, D. (2008). "Assessing the outdoor operating temperature of + photovoltaic modules." Progress in Photovoltaics 16(4): 307-315. - [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. + .. [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. + .. [3] "IEC 61853-3 Photovoltaic (PV) module performance testing and energy + rating - Part 3: Energy rating of PV modules". IEC, Geneva, 2018. ''' # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Dec., 2019 diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index b48a3e05ba..852a58a085 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -113,6 +113,31 @@ def pvwatts_dc_pvwatts_ac_system(sapm_temperature_cs5p_220m): return system +@pytest.fixture(scope="function") +def pvwatts_dc_pvwatts_ac_faiman_temp_system(): + module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003} + temp_model_params = {'u0': 25.0, 'u1': 6.84} + inverter_parameters = {'pdc0': 220, 'eta_inv_nom': 0.95} + system = PVSystem(surface_tilt=32.2, surface_azimuth=180, + module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, + inverter_parameters=inverter_parameters) + return system + + +@pytest.fixture(scope="function") +def pvwatts_dc_pvwatts_ac_pvsyst_temp_system(): + module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003} + temp_model_params = {'u_c': 29.0, 'u_v': 0.0, 'eta_m': 0.1, + 'alpha_absorption': 0.9} + inverter_parameters = {'pdc0': 220, 'eta_inv_nom': 0.95} + system = PVSystem(surface_tilt=32.2, surface_azimuth=180, + module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, + inverter_parameters=inverter_parameters) + return system + + @pytest.fixture(scope="function") def system_no_aoi(cec_module_cs5p_220m, sapm_temperature_cs5p_220m, cec_inverter_parameters): @@ -230,10 +255,10 @@ def test_run_model_gueymard_perez(system, location): assert_series_equal(ac, expected) -def test_run_model_with_weather(system, location, weather, mocker): +def test_run_model_with_weather_sapm_temp(system, location, weather, mocker): + # test with sapm cell temperature model weather['wind_speed'] = 5 weather['temp_air'] = 10 - # test with sapm cell temperature model system.racking_model = 'open_rack' system.module_type = 'glass_glass' mc = ModelChain(system, location) @@ -246,7 +271,12 @@ def test_run_model_with_weather(system, location, weather, mocker): assert_series_equal(m_sapm.call_args[0][1], weather['temp_air']) # temp assert_series_equal(m_sapm.call_args[0][2], weather['wind_speed']) # wind assert not mc.ac.empty + + +def test_run_model_with_weather_pvsyst_temp(system, location, weather, mocker): # test with pvsyst cell temperature model + weather['wind_speed'] = 5 + weather['temp_air'] = 10 system.racking_model = 'freestanding' system.temperature_model_parameters = \ temperature._temperature_model_params('pvsyst', 'freestanding') @@ -260,6 +290,21 @@ def test_run_model_with_weather(system, location, weather, mocker): assert not mc.ac.empty +def test_run_model_with_weather_faiman_temp(system, location, weather, mocker): + # test with faiman cell temperature model + weather['wind_speed'] = 5 + weather['temp_air'] = 10 + system.temperature_model_parameters = {'u0': 25.0, 'u1': 6.84} + mc = ModelChain(system, location) + mc.temperature_model = 'faiman' + m_faiman = mocker.spy(system, 'faiman_celltemp') + mc.run_model(weather) + assert m_faiman.call_count == 1 + assert_series_equal(m_faiman.call_args[0][1], weather['temp_air']) + assert_series_equal(m_faiman.call_args[0][2], weather['wind_speed']) + assert not mc.ac.empty + + def test_run_model_tracker(system, location, weather, mocker): system = SingleAxisTracker( module_parameters=system.module_parameters, @@ -341,15 +386,20 @@ def test_infer_spectral_model(location, system, cec_dc_snl_ac_system, @pytest.mark.parametrize('temp_model', [ - 'sapm', pytest.param('pvsyst', marks=requires_scipy)]) -def test_infer_temp_model(location, system, pvsyst_dc_snl_ac_system, + 'sapm_temp', 'faiman_temp', + pytest.param('pvsyst_temp', marks=requires_scipy)]) +def test_infer_temp_model(location, system, + pvwatts_dc_pvwatts_ac_pvsyst_temp_system, + pvwatts_dc_pvwatts_ac_faiman_temp_system, temp_model): - dc_systems = {'sapm': system, - 'pvsyst': pvsyst_dc_snl_ac_system} + dc_systems = {'sapm_temp': system, + 'pvsyst_temp': pvwatts_dc_pvwatts_ac_pvsyst_temp_system, + 'faiman_temp': pvwatts_dc_pvwatts_ac_faiman_temp_system} system = dc_systems[temp_model] mc = ModelChain(system, location, orientation_strategy='None', aoi_model='physical', spectral_model='no_loss') + assert temp_model == mc.temperature_model.__name__ assert isinstance(mc, ModelChain) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 97a60e4c19..51767885b4 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -415,23 +415,17 @@ def test_PVSystem_pvsyst_celltemp(mocker): assert (out < 90) and (out > 70) -def test_PVSystem_pvsyst_celltemp_kwargs(mocker): - temp_model_params = temperature.TEMPERATURE_MODEL_PARAMETERS['pvsyst'][ - 'insulated'] - alpha_absorption = 0.85 - eta_m = 0.17 - module_parameters = {'alpha_absorption': alpha_absorption, 'eta_m': eta_m} - system = pvsystem.PVSystem(module_parameters=module_parameters, - temperature_model_parameters=temp_model_params) - mocker.spy(temperature, 'pvsyst_cell') - irrad = 800 - temp = 45 - wind = 0.5 - out = system.pvsyst_celltemp(irrad, temp, wind_speed=wind) - temperature.pvsyst_cell.assert_called_once_with( - irrad, temp, wind, temp_model_params['u_c'], temp_model_params['u_v'], - eta_m, alpha_absorption) - assert (out < 90) and (out > 70) +def test_PVSystem_faiman_celltemp(mocker): + u0, u1 = 25.0, 6.84 # default values + temp_model_params = {'u0': u0, 'u1': u1} + system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params) + mocker.spy(temperature, 'faiman') + temps = 25 + irrads = 1000 + winds = 1 + out = system.faiman_celltemp(irrads, temps, winds) + temperature.faiman.assert_called_once_with(irrads, temps, winds, u0, u1) + assert_allclose(out, 56.4, atol=1) def test__infer_temperature_model_params():