diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index a82471c713..37bb09113b 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -242,6 +242,8 @@ PV temperature models pvsystem.PVSystem.sapm_celltemp pvsystem.PVSystem.pvsyst_celltemp pvsystem.PVSystem.faiman_celltemp + pvsystem.PVSystem.fuentes_celltemp + pvsystem.PVSystem.noct_sam_celltemp Temperature Model Parameters ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index cb8a34756e..2c8e178eba 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -102,7 +102,7 @@ Enhancements from DC power. Use parameter ``model`` to specify which inverter model to use. (:pull:`1147`, :issue:`998`, :pull:`1150`) * Added :py:func:`~pvlib.temperature.noct_sam`, a cell temperature model - implemented in SAM (:pull:`1177`) + implemented in SAM (:pull:`1177`, :pull:`1195`) Bug fixes ~~~~~~~~~ diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 990598ae1e..1547df18e6 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -352,7 +352,7 @@ class ModelChain: as the first argument to a user-defined function. temperature_model: None, str or function, default None - Valid strings are 'sapm', 'pvsyst', 'faiman', and 'fuentes'. + Valid strings are: 'sapm', 'pvsyst', 'faiman', 'fuentes', 'noct_sam'. The ModelChain instance will be passed as the first argument to a user-defined function. @@ -935,6 +935,8 @@ def temperature_model(self, model): self._temperature_model = self.faiman_temp elif model == 'fuentes': self._temperature_model = self.fuentes_temp + elif model == 'noct_sam': + self._temperature_model = self.noct_sam_temp else: raise ValueError(model + ' is not a valid temperature model') # check system.temperature_model_parameters for consistency @@ -965,6 +967,8 @@ def infer_temperature_model(self): return self.faiman_temp elif {'noct_installed'} <= params: return self.fuentes_temp + elif {'noct', 'eta_m_ref'} <= params: + return self.noct_sam_temp else: raise ValueError(f'could not infer temperature model from ' f'system.temperature_model_parameters. Check ' @@ -994,7 +998,11 @@ def _set_celltemp(self, model): self.results.effective_irradiance) temp_air = _tuple_from_dfs(self.weather, 'temp_air') wind_speed = _tuple_from_dfs(self.weather, 'wind_speed') - self.results.cell_temperature = model(poa, temp_air, wind_speed) + arg_list = [poa, temp_air, wind_speed] + kwargs = {} + if model == self.system.noct_sam_celltemp: + kwargs['effective_irradiance'] = self.results.effective_irradiance + self.results.cell_temperature = model(*tuple(arg_list)) return self def sapm_temp(self): @@ -1009,6 +1017,9 @@ def faiman_temp(self): def fuentes_temp(self): return self._set_celltemp(self.system.fuentes_celltemp) + def noct_sam_temp(self): + return self._set_celltemp(self.system.noct_sam_celltemp) + @property def losses_model(self): return self._losses_model diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index a727673265..3314887a2b 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -143,7 +143,8 @@ class PVSystem: Module parameters as defined by the SAPM, CEC, or other. temperature_model_parameters : None, dict or Series, default None. - Temperature model parameters as defined by the SAPM, Pvsyst, or other. + Temperature model parameters as required by one of the models in + pvlib.temperature (excluding poa_global, temp_air and wind_speed). modules_per_string: int or float, default 1 See system topology discussion above. @@ -750,8 +751,6 @@ def fuentes_celltemp(self, poa_global, temp_air, wind_speed): if you want to match the PVWatts behavior, you can override it by including a ``surface_tilt`` value in ``temperature_model_parameters``. - Notes - ----- The `temp_air` and `wind_speed` parameters may be passed as tuples to provide different values for each Array in the system. If not passed as a tuple then the same value is used for input to each Array. @@ -781,6 +780,82 @@ def _build_kwargs_fuentes(array): ) ) + @_unwrap_single_value + def noct_sam_celltemp(self, poa_global, temp_air, wind_speed, + effective_irradiance=None): + """ + Use :py:func:`temperature.noct_sam` to calculate cell temperature. + + Parameters + ---------- + poa_global : numeric or tuple of numeric + Total incident irradiance in W/m^2. + + temp_air : numeric or tuple of numeric + Ambient dry bulb temperature in degrees C. + + wind_speed : numeric or tuple of numeric + Wind speed in m/s at a height of 10 meters. + + effective_irradiance : numeric, tuple of numeric, or None. + The irradiance that is converted to photocurrent. If None, + assumed equal to ``poa_global``. [W/m^2] + + Returns + ------- + temperature_cell : numeric or tuple of numeric + The modeled cell temperature [C] + + Notes + ----- + The `temp_air` and `wind_speed` parameters may be passed as tuples + to provide different values for each Array in the system. If not + passed as a tuple then the same value is used for input to each Array. + If passed as a tuple the length must be the same as the number of + Arrays. + """ + # default to using the Array attribute, but allow user to + # override with a custom surface_tilt value + poa_global = self._validate_per_array(poa_global) + temp_air = self._validate_per_array(temp_air, system_wide=True) + wind_speed = self._validate_per_array(wind_speed, system_wide=True) + + # need effective_irradiance to be an iterable + if effective_irradiance is None: + effective_irradiance = tuple([None] * self.num_arrays) + else: + effective_irradiance = self._validate_per_array( + effective_irradiance) + + def _build_kwargs_noct_sam(array): + temp_model_kwargs = _build_kwargs([ + 'transmittance_absorptance', + 'array_height', 'mount_standoff'], + array.temperature_model_parameters) + try: + # noct_sam required args + # bundled with kwargs for simplicity + temp_model_kwargs['noct'] = \ + array.temperature_model_parameters['noct'] + temp_model_kwargs['eta_m_ref'] = \ + array.temperature_model_parameters['eta_m_ref'] + except KeyError: + msg = ('Parameters noct and eta_m_ref are required.' + ' Found {} in temperature_model_parameters.' + .format(array.temperature_model_parameters)) + raise KeyError(msg) + return temp_model_kwargs + return tuple( + temperature.noct_sam( + poa_global, temp_air, wind_speed, + effective_irradiance=eff_irrad, + **_build_kwargs_noct_sam(array)) + for array, poa_global, temp_air, wind_speed, eff_irrad in zip( + self.arrays, poa_global, temp_air, wind_speed, + effective_irradiance + ) + ) + @_unwrap_single_value def first_solar_spectral_loss(self, pw, airmass_absolute): diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index d4cb14814e..28e7889414 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -223,6 +223,18 @@ def pvwatts_dc_pvwatts_ac_fuentes_temp_system(): return system +@pytest.fixture(scope="function") +def pvwatts_dc_pvwatts_ac_noct_sam_temp_system(): + module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003} + temp_model_params = {'noct': 45, 'eta_m_ref': 0.2} + 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): @@ -693,6 +705,23 @@ def test_run_model_with_weather_fuentes_temp(sapm_dc_snl_ac_system, location, assert not mc.results.ac.empty +def test_run_model_with_weather_noct_sam_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': 45, 'eta_m_ref': 0.2 + } + mc = ModelChain(sapm_dc_snl_ac_system, location) + mc.temperature_model = 'noct_sam' + m_noct_sam = mocker.spy(sapm_dc_snl_ac_system, 'noct_sam_celltemp') + mc.run_model(weather) + assert m_noct_sam.call_count == 1 + assert_series_equal(m_noct_sam.call_args[0][1], weather['temp_air']) + assert_series_equal(m_noct_sam.call_args[0][2], weather['wind_speed']) + assert not mc.results.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, @@ -907,7 +936,9 @@ def test__prepare_temperature_arrays_weather(sapm_dc_snl_ac_system_same_arrays, ({'u0': 25.0, 'u1': 6.84}, ModelChain.faiman_temp), ({'noct_installed': 45}, - ModelChain.fuentes_temp)]) + ModelChain.fuentes_temp), + ({'noct': 45, 'eta_m_ref': 0.2}, + ModelChain.noct_sam_temp)]) def test_temperature_models_arrays_multi_weather( temp_params, temp_model, sapm_dc_snl_ac_system_same_arrays, @@ -1256,16 +1287,19 @@ def test_infer_spectral_model(location, sapm_dc_snl_ac_system, @pytest.mark.parametrize('temp_model', [ - 'sapm_temp', 'faiman_temp', 'pvsyst_temp', 'fuentes_temp']) + 'sapm_temp', 'faiman_temp', 'pvsyst_temp', 'fuentes_temp', + 'noct_sam_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, + pvwatts_dc_pvwatts_ac_noct_sam_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, - 'fuentes_temp': pvwatts_dc_pvwatts_ac_fuentes_temp_system} + 'fuentes_temp': pvwatts_dc_pvwatts_ac_fuentes_temp_system, + 'noct_sam_temp': pvwatts_dc_pvwatts_ac_noct_sam_temp_system} system = dc_systems[temp_model] mc = ModelChain(system, location, aoi_model='physical', spectral_model='no_loss') diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 5c0ef7b4a2..1c54402039 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -388,7 +388,11 @@ def two_array_system(pvsyst_module_params, cec_module_params): # Need u_v to be non-zero so wind-speed changes cell temperature # under the pvsyst model. temperature_model['u_v'] = 1.0 + # parameter for fuentes temperature model temperature_model['noct_installed'] = 45 + # parameters for noct_sam temperature model + temperature_model['noct'] = 45. + temperature_model['eta_m_ref'] = 0.2 module_params = {**pvsyst_module_params, **cec_module_params} return pvsystem.PVSystem( arrays=[ @@ -495,11 +499,53 @@ def test_PVSystem_faiman_celltemp(mocker): assert_allclose(out, 56.4, atol=1) +def test_PVSystem_noct_celltemp(mocker): + poa_global, temp_air, wind_speed, noct, eta_m_ref = (1000., 25., 1., 45., + 0.2) + expected = 55.230790492 + temp_model_params = {'noct': noct, 'eta_m_ref': eta_m_ref} + system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params) + mocker.spy(temperature, 'noct_sam') + out = system.noct_sam_celltemp(poa_global, temp_air, wind_speed) + temperature.noct_sam.assert_called_once_with( + poa_global, temp_air, wind_speed, effective_irradiance=None, noct=noct, + eta_m_ref=eta_m_ref) + assert_allclose(out, expected) + # dufferent types + out = system.noct_sam_celltemp(np.array(poa_global), np.array(temp_air), + np.array(wind_speed)) + assert_allclose(out, expected) + dr = pd.date_range(start='2020-01-01 12:00:00', end='2020-01-01 13:00:00', + freq='1H') + out = system.noct_sam_celltemp(pd.Series(index=dr, data=poa_global), + pd.Series(index=dr, data=temp_air), + pd.Series(index=dr, data=wind_speed)) + assert_series_equal(out, pd.Series(index=dr, data=expected)) + # now use optional arguments + temp_model_params.update({'transmittance_absorptance': 0.8, + 'array_height': 2, + 'mount_standoff': 2.0}) + expected = 60.477703576 + system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params) + out = system.noct_sam_celltemp(poa_global, temp_air, wind_speed, + effective_irradiance=1100.) + assert_allclose(out, expected) + + +def test_PVSystem_noct_celltemp_error(): + poa_global, temp_air, wind_speed, eta_m_ref = (1000., 25., 1., 0.2) + temp_model_params = {'eta_m_ref': eta_m_ref} + system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params) + with pytest.raises(KeyError): + system.noct_sam_celltemp(poa_global, temp_air, wind_speed) + + @pytest.mark.parametrize("celltemp", [pvsystem.PVSystem.faiman_celltemp, pvsystem.PVSystem.pvsyst_celltemp, pvsystem.PVSystem.sapm_celltemp, - pvsystem.PVSystem.fuentes_celltemp]) + pvsystem.PVSystem.fuentes_celltemp, + pvsystem.PVSystem.noct_sam_celltemp]) def test_PVSystem_multi_array_celltemp_functions(celltemp, two_array_system): times = pd.date_range(start='2020-08-25 11:00', freq='H', periods=3) irrad_one = pd.Series(1000, index=times) @@ -515,7 +561,8 @@ def test_PVSystem_multi_array_celltemp_functions(celltemp, two_array_system): [pvsystem.PVSystem.faiman_celltemp, pvsystem.PVSystem.pvsyst_celltemp, pvsystem.PVSystem.sapm_celltemp, - pvsystem.PVSystem.fuentes_celltemp]) + pvsystem.PVSystem.fuentes_celltemp, + pvsystem.PVSystem.noct_sam_celltemp]) def test_PVSystem_multi_array_celltemp_multi_temp(celltemp, two_array_system): times = pd.date_range(start='2020-08-25 11:00', freq='H', periods=3) irrad = pd.Series(1000, index=times) @@ -543,7 +590,8 @@ def test_PVSystem_multi_array_celltemp_multi_temp(celltemp, two_array_system): [pvsystem.PVSystem.faiman_celltemp, pvsystem.PVSystem.pvsyst_celltemp, pvsystem.PVSystem.sapm_celltemp, - pvsystem.PVSystem.fuentes_celltemp]) + pvsystem.PVSystem.fuentes_celltemp, + pvsystem.PVSystem.noct_sam_celltemp]) def test_PVSystem_multi_array_celltemp_multi_wind(celltemp, two_array_system): times = pd.date_range(start='2020-08-25 11:00', freq='H', periods=3) irrad = pd.Series(1000, index=times) @@ -571,7 +619,8 @@ def test_PVSystem_multi_array_celltemp_multi_wind(celltemp, two_array_system): [pvsystem.PVSystem.faiman_celltemp, pvsystem.PVSystem.pvsyst_celltemp, pvsystem.PVSystem.sapm_celltemp, - pvsystem.PVSystem.fuentes_celltemp]) + pvsystem.PVSystem.fuentes_celltemp, + pvsystem.PVSystem.noct_sam_celltemp]) def test_PVSystem_multi_array_celltemp_temp_too_short( celltemp, two_array_system): with pytest.raises(ValueError, @@ -583,7 +632,8 @@ def test_PVSystem_multi_array_celltemp_temp_too_short( [pvsystem.PVSystem.faiman_celltemp, pvsystem.PVSystem.pvsyst_celltemp, pvsystem.PVSystem.sapm_celltemp, - pvsystem.PVSystem.fuentes_celltemp]) + pvsystem.PVSystem.fuentes_celltemp, + pvsystem.PVSystem.noct_sam_celltemp]) def test_PVSystem_multi_array_celltemp_temp_too_long( celltemp, two_array_system): with pytest.raises(ValueError, @@ -595,7 +645,8 @@ def test_PVSystem_multi_array_celltemp_temp_too_long( [pvsystem.PVSystem.faiman_celltemp, pvsystem.PVSystem.pvsyst_celltemp, pvsystem.PVSystem.sapm_celltemp, - pvsystem.PVSystem.fuentes_celltemp]) + pvsystem.PVSystem.fuentes_celltemp, + pvsystem.PVSystem.noct_sam_celltemp]) def test_PVSystem_multi_array_celltemp_wind_too_short( celltemp, two_array_system): with pytest.raises(ValueError, @@ -607,7 +658,8 @@ def test_PVSystem_multi_array_celltemp_wind_too_short( [pvsystem.PVSystem.faiman_celltemp, pvsystem.PVSystem.pvsyst_celltemp, pvsystem.PVSystem.sapm_celltemp, - pvsystem.PVSystem.fuentes_celltemp]) + pvsystem.PVSystem.fuentes_celltemp, + pvsystem.PVSystem.noct_sam_celltemp]) def test_PVSystem_multi_array_celltemp_wind_too_long( celltemp, two_array_system): with pytest.raises(ValueError, @@ -618,8 +670,9 @@ def test_PVSystem_multi_array_celltemp_wind_too_long( @pytest.mark.parametrize("celltemp", [pvsystem.PVSystem.faiman_celltemp, pvsystem.PVSystem.pvsyst_celltemp, + pvsystem.PVSystem.sapm_celltemp, pvsystem.PVSystem.fuentes_celltemp, - pvsystem.PVSystem.sapm_celltemp]) + pvsystem.PVSystem.noct_sam_celltemp]) def test_PVSystem_multi_array_celltemp_poa_length_mismatch( celltemp, two_array_system): with pytest.raises(ValueError, diff --git a/pvlib/tests/test_temperature.py b/pvlib/tests/test_temperature.py index 1ce2e69a47..4c8574ffe6 100644 --- a/pvlib/tests/test_temperature.py +++ b/pvlib/tests/test_temperature.py @@ -271,12 +271,12 @@ def test_noct_sam_options(): poa_global, temp_air, wind_speed, noct, eta_m_ref = (1000., 25., 1., 45., 0.2) effective_irradiance = 1100. - transmittance_absorbtance = 0.8 + transmittance_absorptance = 0.8 array_height = 2 mount_standoff = 2.0 result = temperature.noct_sam(poa_global, temp_air, wind_speed, noct, eta_m_ref, effective_irradiance, - transmittance_absorbtance, array_height, + transmittance_absorptance, array_height, mount_standoff) expected = 60.477703576 assert_allclose(result, expected)