diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 2e882e9a46..845ad393fa 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -627,6 +627,7 @@ ModelChain model definitions. modelchain.ModelChain.sapm_temp modelchain.ModelChain.pvsyst_temp modelchain.ModelChain.faiman_temp + modelchain.ModelChain.fuentes_temp modelchain.ModelChain.pvwatts_losses modelchain.ModelChain.no_extra_losses diff --git a/docs/sphinx/source/whatsnew/v0.8.1.rst b/docs/sphinx/source/whatsnew/v0.8.1.rst index 9b6e2a3800..27e5498596 100644 --- a/docs/sphinx/source/whatsnew/v0.8.1.rst +++ b/docs/sphinx/source/whatsnew/v0.8.1.rst @@ -13,6 +13,8 @@ Deprecations Enhancements ~~~~~~~~~~~~ +* Create :py:func:`~pvlib.pvsystem.PVSystem.fuentes_celltemp` and add ``temperature_model='fuentes'`` + option to :py:class:`~pvlib.modelchain.ModelChain`. (:pull:`1042`) (:issue:`1073`) * Added :py:func:`pvlib.temperature.ross` for cell temperature modeling using only NOCT. (:pull:`1045`) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 7077d6c920..e5becc4a1a 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -323,9 +323,9 @@ class ModelChain: as the first argument to a user-defined function. temperature_model: None, str or function, default None - Valid strings are 'sapm', 'pvsyst', and 'faiman'. The ModelChain - instance will be passed as the first argument to a user-defined - function. + Valid strings are 'sapm', 'pvsyst', 'faiman', and 'fuentes'. + 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 @@ -866,6 +866,8 @@ def temperature_model(self, model): self._temperature_model = self.pvsyst_temp elif model == 'faiman': self._temperature_model = self.faiman_temp + elif model == 'fuentes': + self._temperature_model = self.fuentes_temp else: raise ValueError(model + ' is not a valid temperature model') # check system.temperature_model_parameters for consistency @@ -891,6 +893,8 @@ def infer_temperature_model(self): return self.pvsyst_temp elif {'u0', 'u1'} <= params: return self.faiman_temp + elif {'noct_installed'} <= params: + return self.fuentes_temp else: raise ValueError('could not infer temperature model from ' 'system.temperature_module_parameters {}.' @@ -914,6 +918,12 @@ def faiman_temp(self): self.weather['wind_speed']) return self + def fuentes_temp(self): + self.cell_temperature = self.system.fuentes_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 fecc93075e..c4a38cf65d 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -609,6 +609,46 @@ def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): return temperature.faiman(poa_global, temp_air, wind_speed, **kwargs) + def fuentes_celltemp(self, poa_global, temp_air, wind_speed): + """ + Use :py:func:`temperature.fuentes` to calculate cell temperature. + + Parameters + ---------- + poa_global : pandas Series + Total incident irradiance [W/m^2] + + temp_air : pandas Series + Ambient dry bulb temperature [C] + + wind_speed : pandas Series + Wind speed [m/s] + + Returns + ------- + temperature_cell : pandas Series + The modeled cell temperature [C] + + Notes + ----- + The Fuentes thermal model uses the module surface tilt for convection + modeling. The SAM implementation of PVWatts hardcodes the surface tilt + value at 30 degrees, ignoring whatever value is used for irradiance + transposition. This method defaults to using ``self.surface_tilt``, but + if you want to match the PVWatts behavior, you can override it by + including a ``surface_tilt`` value in ``temperature_model_parameters``. + """ + # default to using the PVSystem attribute, but allow user to + # override with a custom surface_tilt value + kwargs = {'surface_tilt': self.surface_tilt} + temp_model_kwargs = _build_kwargs([ + 'noct_installed', 'module_height', 'wind_height', 'emissivity', + 'absorption', 'surface_tilt', 'module_width', 'module_length'], + self.temperature_model_parameters) + kwargs.update(temp_model_kwargs) + return temperature.fuentes(poa_global, temp_air, wind_speed, + **kwargs) + def first_solar_spectral_loss(self, pw, airmass_absolute): """ diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 4393c5091d..d02611bbf3 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -138,6 +138,18 @@ def pvwatts_dc_pvwatts_ac_pvsyst_temp_system(): return system +@pytest.fixture(scope="function") +def pvwatts_dc_pvwatts_ac_fuentes_temp_system(): + module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003} + temp_model_params = {'noct_installed': 45} + 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): @@ -317,6 +329,23 @@ def test_run_model_with_weather_faiman_temp(sapm_dc_snl_ac_system, location, assert not mc.ac.empty +def test_run_model_with_weather_fuentes_temp(sapm_dc_snl_ac_system, location, + weather, mocker): + weather['wind_speed'] = 5 + weather['temp_air'] = 10 + sapm_dc_snl_ac_system.temperature_model_parameters = { + 'noct_installed': 45 + } + mc = ModelChain(sapm_dc_snl_ac_system, location) + mc.temperature_model = 'fuentes' + m_fuentes = mocker.spy(sapm_dc_snl_ac_system, 'fuentes_celltemp') + mc.run_model(weather) + assert m_fuentes.call_count == 1 + assert_series_equal(m_fuentes.call_args[0][1], weather['temp_air']) + assert_series_equal(m_fuentes.call_args[0][2], weather['wind_speed']) + assert not mc.ac.empty + + def test_run_model_tracker(sapm_dc_snl_ac_system, location, weather, mocker): system = SingleAxisTracker( module_parameters=sapm_dc_snl_ac_system.module_parameters, @@ -479,14 +508,16 @@ def test_infer_spectral_model(location, sapm_dc_snl_ac_system, @pytest.mark.parametrize('temp_model', [ - 'sapm_temp', 'faiman_temp', 'pvsyst_temp']) + 'sapm_temp', 'faiman_temp', 'pvsyst_temp', 'fuentes_temp']) def test_infer_temp_model(location, sapm_dc_snl_ac_system, pvwatts_dc_pvwatts_ac_pvsyst_temp_system, pvwatts_dc_pvwatts_ac_faiman_temp_system, + pvwatts_dc_pvwatts_ac_fuentes_temp_system, temp_model): dc_systems = {'sapm_temp': sapm_dc_snl_ac_system, 'pvsyst_temp': pvwatts_dc_pvwatts_ac_pvsyst_temp_system, - 'faiman_temp': pvwatts_dc_pvwatts_ac_faiman_temp_system} + 'faiman_temp': pvwatts_dc_pvwatts_ac_faiman_temp_system, + 'fuentes_temp': pvwatts_dc_pvwatts_ac_fuentes_temp_system} system = dc_systems[temp_model] mc = ModelChain(system, location, orientation_strategy='None', aoi_model='physical', diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index b9edf54fff..5675f8c3f7 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -360,6 +360,50 @@ def test_PVSystem_faiman_celltemp(mocker): assert_allclose(out, 56.4, atol=1) +def test_PVSystem_fuentes_celltemp(mocker): + noct_installed = 45 + temp_model_params = {'noct_installed': noct_installed} + system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params) + spy = mocker.spy(temperature, 'fuentes') + index = pd.date_range('2019-01-01 11:00', freq='h', periods=3) + temps = pd.Series(25, index) + irrads = pd.Series(1000, index) + winds = pd.Series(1, index) + out = system.fuentes_celltemp(irrads, temps, winds) + assert_series_equal(spy.call_args[0][0], irrads) + assert_series_equal(spy.call_args[0][1], temps) + assert_series_equal(spy.call_args[0][2], winds) + assert spy.call_args[1]['noct_installed'] == noct_installed + assert_series_equal(out, pd.Series([52.85, 55.85, 55.85], index, + name='tmod')) + + +def test_PVSystem_fuentes_celltemp_override(mocker): + # test that the surface_tilt value in the cell temp calculation can be + # overridden but defaults to the surface_tilt attribute of the PVSystem + spy = mocker.spy(temperature, 'fuentes') + + noct_installed = 45 + index = pd.date_range('2019-01-01 11:00', freq='h', periods=3) + temps = pd.Series(25, index) + irrads = pd.Series(1000, index) + winds = pd.Series(1, index) + + # uses default value + temp_model_params = {'noct_installed': noct_installed} + system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params, + surface_tilt=20) + system.fuentes_celltemp(irrads, temps, winds) + assert spy.call_args[1]['surface_tilt'] == 20 + + # can be overridden + temp_model_params = {'noct_installed': noct_installed, 'surface_tilt': 30} + system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params, + surface_tilt=20) + system.fuentes_celltemp(irrads, temps, winds) + assert spy.call_args[1]['surface_tilt'] == 30 + + def test__infer_temperature_model_params(): system = pvsystem.PVSystem(module_parameters={}, racking_model='open_rack',