diff --git a/docs/sphinx/source/whatsnew/v0.9.2.rst b/docs/sphinx/source/whatsnew/v0.9.2.rst index 9a1e0b6d7b..d3e249d56b 100644 --- a/docs/sphinx/source/whatsnew/v0.9.2.rst +++ b/docs/sphinx/source/whatsnew/v0.9.2.rst @@ -8,6 +8,10 @@ Deprecations Enhancements ~~~~~~~~~~~~ +* albedo can now be provided as a column in the `weather` DataFrame input to + :py:method:`pvlib.modelchain.ModelChain.run_model`. (:issue:`1387`, :pull:`1478`) +* albedo is now available as an input to :py:meth:`pvlib.pvsystem.PVSystem.get_irradiance` + and :py:meth:`pvlib.pvsystem.Array.get_irradiance`. (:pull:`1478`) * :py:func:`pvlib.iotools.read_surfrad` now also accepts remote files with https links in addition to files on the SURFRAD FTP server (:pull:`1459`) @@ -20,14 +24,15 @@ Enhancements * Add support for `PEP517 `_ & `PEP518 `_ with setuptools build backend. (:pull:`1495`) + Bug fixes ~~~~~~~~~ * :py:func:`pvlib.irradiance.get_total_irradiance` and :py:func:`pvlib.solarposition.spa_python` now raise an error instead - of silently ignoring unknown parameters (:pull:`1437`) + of silently ignoring unknown parameters. (:pull:`1437`) * Fix a bug in :py:func:`pvlib.solarposition.sun_rise_set_transit_ephem` where passing localized timezones with large UTC offsets could return - rise/set/transit times for the wrong day in recent versions of ``ephem`` + rise/set/transit times for the wrong day in recent versions of ``ephem``. (:issue:`1449`, :pull:`1448`) * :py:func:`pvlib.iotools.read_tmy3` is now able to accept midnight timestamps as either 24:00 (which is the standard) as well as 00:00. @@ -68,6 +73,7 @@ Contributors * Naman Priyadarshi (:ghuser:`Naman-Priyadarshi`) * Chencheng Luo (:ghuser:`roger-lcc`) * Prajwal Borkar (:ghuser:`PrajwalBorkar`) +* Cliff Hansen (:ghuser:`cwhanse`) * Kevin Anderson (:ghuser:`kanderso-nrel`) * Cliff Hansen (:ghuser:`cwhanse`) * Jules Chéron (:ghuser:`jules-ch`) diff --git a/pvlib/clearsky.py b/pvlib/clearsky.py index 311d683674..2f5cd68c62 100644 --- a/pvlib/clearsky.py +++ b/pvlib/clearsky.py @@ -960,8 +960,8 @@ def bird(zenith, airmass_relative, aod380, aod500, precipitable_water, Extraterrestrial radiation [W/m^2], defaults to 1364[W/m^2] asymmetry : numeric Asymmetry factor, defaults to 0.85 - albedo : numeric - Albedo, defaults to 0.2 + albedo : numeric, default 0.2 + Ground surface albedo. [unitless] Returns ------- diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py index 3de4d96f65..03ddd13f5a 100644 --- a/pvlib/irradiance.py +++ b/pvlib/irradiance.py @@ -304,7 +304,7 @@ def beam_component(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, def get_total_irradiance(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, dni, ghi, dhi, dni_extra=None, airmass=None, - albedo=.25, surface_type=None, + albedo=0.25, surface_type=None, model='isotropic', model_perez='allsitescomposite1990'): r""" @@ -344,7 +344,7 @@ def get_total_irradiance(surface_tilt, surface_azimuth, airmass : None or numeric, default None Relative airmass (not adjusted for pressure). [unitless] albedo : numeric, default 0.25 - Surface albedo. [unitless] + Ground surface albedo. [unitless] surface_type : None or str, default None Surface type. See :py:func:`~pvlib.irradiance.get_ground_diffuse` for the list of accepted values. @@ -1872,7 +1872,7 @@ def gti_dirint(poa_global, aoi, solar_zenith, solar_azimuth, times, applied. albedo : numeric, default 0.25 - Surface albedo + Ground surface albedo. [unitless] model : String, default 'perez' Irradiance model. See :py:func:`get_sky_diffuse` for allowed values. diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 2798c39a68..8211981433 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -268,7 +268,7 @@ class ModelChainResult: _per_array_fields = {'total_irrad', 'aoi', 'aoi_modifier', 'spectral_modifier', 'cell_temperature', 'effective_irradiance', 'dc', 'diode_params', - 'dc_ohmic_losses', 'weather'} + 'dc_ohmic_losses', 'weather', 'albedo'} # system-level information solar_position: Optional[pd.DataFrame] = field(default=None) @@ -366,6 +366,10 @@ class ModelChainResult: """DatetimeIndex containing a copy of the index of the input weather data. """ + albedo: Optional[PerArray[pd.Series]] = None + """Series (or tuple of Series, one for each array) containing albedo. + """ + def _result_type(self, value): """Coerce `value` to the correct type according to ``self._singleton_tuples``.""" @@ -1339,6 +1343,17 @@ def _prep_inputs_solar_pos(self, weather): **kwargs) return self + def _prep_inputs_albedo(self, weather): + """ + Get albedo from weather + """ + try: + self.results.albedo = _tuple_from_dfs(weather, 'albedo') + except KeyError: + self.results.albedo = tuple([ + a.albedo for a in self.system.arrays]) + return self + def _prep_inputs_airmass(self): """ Assign airmass @@ -1471,11 +1486,17 @@ def prepare_inputs(self, weather): Parameters ---------- - weather : DataFrame, or tuple or list of DataFrame + weather : DataFrame, or tuple or list of DataFrames Required column names include ``'dni'``, ``'ghi'``, ``'dhi'``. - Optional column names are ``'wind_speed'``, ``'temp_air'``; if not + Optional column names are ``'wind_speed'``, ``'temp_air'``, + ``'albedo'``. + + If optional columns ``'wind_speed'``, ``'temp_air'`` are not provided, air temperature of 20 C and wind speed - of 0 m/s will be added to the DataFrame. + of 0 m/s will be added to the ``weather`` DataFrame. + + If optional column ``'albedo'`` is provided, albedo values in the + ModelChain's PVSystem.arrays are ignored. If `weather` is a tuple or list, it must be of the same length and order as the Arrays of the ModelChain's PVSystem. @@ -1494,7 +1515,7 @@ def prepare_inputs(self, weather): Notes ----- Assigns attributes to ``results``: ``times``, ``weather``, - ``solar_position``, ``airmass``, ``total_irrad``, ``aoi`` + ``solar_position``, ``airmass``, ``total_irrad``, ``aoi``, ``albedo``. See also -------- @@ -1507,6 +1528,7 @@ def prepare_inputs(self, weather): self._prep_inputs_solar_pos(weather) self._prep_inputs_airmass() + self._prep_inputs_albedo(weather) # PVSystem.get_irradiance and SingleAxisTracker.get_irradiance # and PVSystem.get_aoi and SingleAxisTracker.get_aoi @@ -1531,6 +1553,7 @@ def prepare_inputs(self, weather): _tuple_from_dfs(self.results.weather, 'dni'), _tuple_from_dfs(self.results.weather, 'ghi'), _tuple_from_dfs(self.results.weather, 'dhi'), + albedo=self.results.albedo, airmass=self.results.airmass['airmass_relative'], model=self.transposition_model ) @@ -1724,16 +1747,32 @@ def run_model(self, weather): Parameters ---------- weather : DataFrame, or tuple or list of DataFrame - Irradiance column names must include ``'dni'``, ``'ghi'``, and - ``'dhi'``. If optional columns ``'temp_air'`` and ``'wind_speed'`` + Column names must include: + + - ``'dni'`` + - ``'ghi'`` + - ``'dhi'`` + + Optional columns are: + + - ``'temp_air'`` + - ``'cell_temperature'`` + - ``'module_temperature'`` + - ``'wind_speed'`` + - ``'albedo'`` + + If optional columns ``'temp_air'`` and ``'wind_speed'`` are not provided, air temperature of 20 C and wind speed of 0 m/s are added to the DataFrame. If optional column ``'cell_temperature'`` is provided, these values are used instead - of `temperature_model`. If optional column `module_temperature` - is provided, `temperature_model` must be ``'sapm'``. + of `temperature_model`. If optional column ``'module_temperature'`` + is provided, ``temperature_model`` must be ``'sapm'``. - If list or tuple, must be of the same length and order as the - Arrays of the ModelChain's PVSystem. + If optional column ``'albedo'`` is provided, ``'albedo'`` may not + be present on the ModelChain's PVSystem.Arrays. + + If weather is a list or tuple, it must be of the same length and + order as the Arrays of the ModelChain's PVSystem. Returns ------- diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 6bb89f34a3..77560e04c0 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -134,7 +134,7 @@ class PVSystem: a single array is created from the other parameters (e.g. `surface_tilt`, `surface_azimuth`). Must contain at least one Array, if length of arrays is 0 a ValueError is raised. If `arrays` is - specified the following parameters are ignored: + specified the following PVSystem parameters are ignored: - `surface_tilt` - `surface_azimuth` @@ -157,13 +157,14 @@ class PVSystem: North=0, East=90, South=180, West=270. albedo : None or float, default None - The ground albedo. If ``None``, will attempt to use - ``surface_type`` and ``irradiance.SURFACE_ALBEDOS`` - to lookup albedo. + Ground surface albedo. If ``None``, then ``surface_type`` is used + to look up a value in ``irradiance.SURFACE_ALBEDOS``. + If ``surface_type`` is also None then a ground surface albedo + of 0.25 is used. surface_type : None or string, default None - The ground surface type. See ``irradiance.SURFACE_ALBEDOS`` - for valid values. + The ground surface type. See ``irradiance.SURFACE_ALBEDOS`` for + valid values. module : None or string, default None The model name of the modules. @@ -333,30 +334,33 @@ def get_aoi(self, solar_zenith, solar_azimuth): @_unwrap_single_value def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, - dni_extra=None, airmass=None, model='haydavies', - **kwargs): + dni_extra=None, airmass=None, albedo=None, + model='haydavies', **kwargs): """ Uses the :py:func:`irradiance.get_total_irradiance` function to - calculate the plane of array irradiance components on a tilted - surface defined by ``self.surface_tilt``, - ``self.surface_azimuth``, and ``self.albedo``. + calculate the plane of array irradiance components on the tilted + surfaces defined by each array's ``surface_tilt`` and + ``surface_azimuth``. Parameters ---------- - solar_zenith : float or Series. + solar_zenith : float or Series Solar zenith angle. - solar_azimuth : float or Series. + solar_azimuth : float or Series Solar azimuth angle. dni : float or Series or tuple of float or Series - Direct Normal Irradiance + Direct Normal Irradiance. [W/m2] ghi : float or Series or tuple of float or Series - Global horizontal irradiance + Global horizontal irradiance. [W/m2] dhi : float or Series or tuple of float or Series - Diffuse horizontal irradiance - dni_extra : None, float or Series, default None - Extraterrestrial direct normal irradiance + Diffuse horizontal irradiance. [W/m2] + dni_extra : None, float, Series or tuple of float or Series,\ + default None + Extraterrestrial direct normal irradiance. [W/m2] airmass : None, float or Series, default None - Airmass + Airmass. [unitless] + albedo : None, float or Series, default None + Ground surface albedo. [unitless] model : String, default 'haydavies' Irradiance model. @@ -376,17 +380,24 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, poa_irradiance : DataFrame or tuple of DataFrame Column names are: ``'poa_global', 'poa_direct', 'poa_diffuse', 'poa_sky_diffuse', 'poa_ground_diffuse'``. + + See also + -------- + pvlib.irradiance.get_total_irradiance """ dni = self._validate_per_array(dni, system_wide=True) ghi = self._validate_per_array(ghi, system_wide=True) dhi = self._validate_per_array(dhi, system_wide=True) + + albedo = self._validate_per_array(albedo, system_wide=True) + return tuple( array.get_irradiance(solar_zenith, solar_azimuth, dni, ghi, dhi, - dni_extra, airmass, model, - **kwargs) - for array, dni, ghi, dhi in zip( - self.arrays, dni, ghi, dhi + dni_extra=dni_extra, airmass=airmass, + albedo=albedo, model=model, **kwargs) + for array, dni, ghi, dhi, albedo in zip( + self.arrays, dni, ghi, dhi, albedo ) ) @@ -1258,14 +1269,14 @@ class Array: If not provided, a FixedMount with zero tilt is used. albedo : None or float, default None - The ground albedo. If ``None``, will attempt to use - ``surface_type`` to look up an albedo value in - ``irradiance.SURFACE_ALBEDOS``. If a surface albedo - cannot be found then 0.25 is used. + Ground surface albedo. If ``None``, then ``surface_type`` is used + to look up a value in ``irradiance.SURFACE_ALBEDOS``. + If ``surface_type`` is also None then a ground surface albedo + of 0.25 is used. surface_type : None or string, default None - The ground surface type. See ``irradiance.SURFACE_ALBEDOS`` - for valid values. + The ground surface type. See ``irradiance.SURFACE_ALBEDOS`` for valid + values. module : None or string, default None The model name of the modules. @@ -1425,15 +1436,14 @@ def get_aoi(self, solar_zenith, solar_azimuth): solar_zenith, solar_azimuth) def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, - dni_extra=None, airmass=None, model='haydavies', - **kwargs): + dni_extra=None, airmass=None, albedo=None, + model='haydavies', **kwargs): """ Get plane of array irradiance components. Uses the :py:func:`pvlib.irradiance.get_total_irradiance` function to calculate the plane of array irradiance components for a surface - defined by ``self.surface_tilt`` and ``self.surface_azimuth`` with - albedo ``self.albedo``. + defined by ``self.surface_tilt`` and ``self.surface_azimuth``. Parameters ---------- @@ -1442,15 +1452,17 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, solar_azimuth : float or Series. Solar azimuth angle. dni : float or Series - Direct Normal Irradiance - ghi : float or Series + Direct normal irradiance. [W/m2] + ghi : float or Series. [W/m2] Global horizontal irradiance dhi : float or Series - Diffuse horizontal irradiance + Diffuse horizontal irradiance. [W/m2] dni_extra : None, float or Series, default None - Extraterrestrial direct normal irradiance + Extraterrestrial direct normal irradiance. [W/m2] airmass : None, float or Series, default None - Airmass + Airmass. [unitless] + albedo : None, float or Series, default None + Ground surface albedo. [unitless] model : String, default 'haydavies' Irradiance model. @@ -1463,7 +1475,14 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, poa_irradiance : DataFrame Column names are: ``'poa_global', 'poa_direct', 'poa_diffuse', 'poa_sky_diffuse', 'poa_ground_diffuse'``. + + See also + -------- + :py:func:`pvlib.irradiance.get_total_irradiance` """ + if albedo is None: + albedo = self.albedo + # not needed for all models, but this is easier if dni_extra is None: dni_extra = irradiance.get_extra_radiation(solar_zenith.index) @@ -1478,8 +1497,8 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, dni, ghi, dhi, dni_extra=dni_extra, airmass=airmass, + albedo=albedo, model=model, - albedo=self.albedo, **kwargs) def get_iam(self, aoi, iam_model='physical'): @@ -3293,7 +3312,7 @@ def dc_ohms_from_percent(vmp_ref, imp_ref, dc_ohmic_percent, See Also -------- - :py:func:`~pvlib.pvsystem.dc_ohmic_losses` + pvlib.pvsystem.dc_ohmic_losses References ---------- @@ -3328,7 +3347,7 @@ def dc_ohmic_losses(resistance, current): See Also -------- - :py:func:`~pvlib.pvsystem.dc_ohms_from_percent` + pvlib.pvsystem.dc_ohms_from_percent References ---------- diff --git a/pvlib/tests/test_clearsky.py b/pvlib/tests/test_clearsky.py index 15fc74e383..d603cbcdfe 100644 --- a/pvlib/tests/test_clearsky.py +++ b/pvlib/tests/test_clearsky.py @@ -756,6 +756,30 @@ def test_bird(): assert np.allclose( testdata['Dif Hz'].where(dusk, 0.), diffuse_horz[1:48], rtol=1e-3 ) + # repeat test with albedo as a Series + alb_series = pd.Series(0.2, index=times) + irrads = clearsky.bird( + zenith, airmass, aod_380nm, aod_500nm, h2o_cm, o3_cm, press_mB * 100., + etr, b_a, alb_series + ) + Eb, Ebh, Gh, Dh = (irrads[_] for _ in field_names) + direct_beam = pd.Series(np.where(dawn, Eb, 0.), index=times).fillna(0.) + assert np.allclose( + testdata['Direct Beam'].where(dusk, 0.), direct_beam[1:48], rtol=1e-3 + ) + direct_horz = pd.Series(np.where(dawn, Ebh, 0.), index=times).fillna(0.) + assert np.allclose( + testdata['Direct Hz'].where(dusk, 0.), direct_horz[1:48], rtol=1e-3 + ) + global_horz = pd.Series(np.where(dawn, Gh, 0.), index=times).fillna(0.) + assert np.allclose( + testdata['Global Hz'].where(dusk, 0.), global_horz[1:48], rtol=1e-3 + ) + diffuse_horz = pd.Series(np.where(dawn, Dh, 0.), index=times).fillna(0.) + assert np.allclose( + testdata['Dif Hz'].where(dusk, 0.), diffuse_horz[1:48], rtol=1e-3 + ) + # test keyword parameters irrads2 = clearsky.bird( zenith, airmass, aod_380nm, aod_500nm, h2o_cm, dni_extra=etr diff --git a/pvlib/tests/test_irradiance.py b/pvlib/tests/test_irradiance.py index 80986f26c3..8dc4877d0d 100644 --- a/pvlib/tests/test_irradiance.py +++ b/pvlib/tests/test_irradiance.py @@ -120,29 +120,38 @@ def test_get_extra_radiation_invalid(): irradiance.get_extra_radiation(300, method='invalid') -def test_grounddiffuse_simple_float(): +def test_get_ground_diffuse_simple_float(): result = irradiance.get_ground_diffuse(40, 900) assert_allclose(result, 26.32000014911496) -def test_grounddiffuse_simple_series(irrad_data): +def test_get_ground_diffuse_simple_series(irrad_data): ground_irrad = irradiance.get_ground_diffuse(40, irrad_data['ghi']) assert ground_irrad.name == 'diffuse_ground' -def test_grounddiffuse_albedo_0(irrad_data): +def test_get_ground_diffuse_albedo_0(irrad_data): ground_irrad = irradiance.get_ground_diffuse( 40, irrad_data['ghi'], albedo=0) assert 0 == ground_irrad.all() +def test_get_ground_diffuse_albedo_series(times): + albedo = pd.Series(0.2, index=times) + ground_irrad = irradiance.get_ground_diffuse( + 45, pd.Series(1000, index=times), albedo) + expected = albedo * 0.5 * (1 - np.sqrt(2) / 2.) * 1000 + expected.name = 'diffuse_ground' + assert_series_equal(ground_irrad, expected) + + def test_grounddiffuse_albedo_invalid_surface(irrad_data): with pytest.raises(KeyError): irradiance.get_ground_diffuse( 40, irrad_data['ghi'], surface_type='invalid') -def test_grounddiffuse_albedo_surface(irrad_data): +def test_get_ground_diffuse_albedo_surface(irrad_data): result = irradiance.get_ground_diffuse(40, irrad_data['ghi'], surface_type='sand') assert_allclose(result, [0, 3.731058, 48.778813, 12.035025], atol=1e-4) @@ -387,6 +396,25 @@ def test_get_total_irradiance(irrad_data, ephem_data, dni_et, 'poa_ground_diffuse'] +@pytest.mark.parametrize('model', ['isotropic', 'klucher', + 'haydavies', 'reindl', 'king', 'perez']) +def test_get_total_irradiance_albedo( + irrad_data, ephem_data, dni_et, relative_airmass, model): + albedo = pd.Series(0.2, index=ephem_data.index) + total = irradiance.get_total_irradiance( + 32, 180, + ephem_data['apparent_zenith'], ephem_data['azimuth'], + dni=irrad_data['dni'], ghi=irrad_data['ghi'], + dhi=irrad_data['dhi'], + dni_extra=dni_et, airmass=relative_airmass, + model=model, + albedo=albedo) + + assert total.columns.tolist() == ['poa_global', 'poa_direct', + 'poa_diffuse', 'poa_sky_diffuse', + 'poa_ground_diffuse'] + + @pytest.mark.parametrize('model', ['isotropic', 'klucher', 'haydavies', 'reindl', 'king', 'perez']) def test_get_total_irradiance_scalars(model): @@ -698,6 +726,14 @@ def test_gti_dirint(): assert_frame_equal(output, expected) + # test with albedo as a Series + albedo = pd.Series(0.05, index=times) + output = irradiance.gti_dirint( + poa_global, aoi, zenith, azimuth, times, surface_tilt, surface_azimuth, + albedo=albedo) + + assert_frame_equal(output, expected) + # test temp_dew input temp_dew = np.array([70, 80, 20]) output = irradiance.gti_dirint( diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index f4a92eadad..62b71f2042 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -495,6 +495,26 @@ def test_prepare_inputs_multi_weather( mc.prepare_inputs(input_type((weather, weather))) num_arrays = sapm_dc_snl_ac_system_Array.num_arrays assert len(mc.results.total_irrad) == num_arrays + # check that albedo is transfered to mc.results from mc.system.arrays + assert mc.results.albedo == (0.2, 0.2) + + +@pytest.mark.parametrize("input_type", [tuple, list]) +def test_prepare_inputs_albedo_in_weather( + sapm_dc_snl_ac_system_Array, location, input_type): + times = pd.date_range(start='20160101 1200-0700', + end='20160101 1800-0700', freq='6H') + mc = ModelChain(sapm_dc_snl_ac_system_Array, location) + weather = pd.DataFrame({'ghi': 1, 'dhi': 1, 'dni': 1, 'albedo': 0.5}, + index=times) + # weather as a single DataFrame + mc.prepare_inputs(weather) + num_arrays = sapm_dc_snl_ac_system_Array.num_arrays + assert len(mc.results.albedo) == num_arrays + # repeat with tuple of weather + mc.prepare_inputs(input_type((weather, weather))) + num_arrays = sapm_dc_snl_ac_system_Array.num_arrays + assert len(mc.results.albedo) == num_arrays def test_prepare_inputs_no_irradiance(sapm_dc_snl_ac_system, location): diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 1141e490e9..7fa013d0dc 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -18,6 +18,7 @@ from pvlib.pvsystem import FixedMount from pvlib import temperature from pvlib._deprecation import pvlibDeprecationWarning +from pvlib.tools import cosd @pytest.mark.parametrize('iam_model,model_params', [ @@ -1673,51 +1674,70 @@ def test_PVSystem_multiple_array_get_aoi(): assert aoi_one > 0 -def test_PVSystem_get_irradiance(): - system = pvsystem.PVSystem(surface_tilt=32, surface_azimuth=135) +@pytest.fixture +def solar_pos(): times = pd.date_range(start='20160101 1200-0700', end='20160101 1800-0700', freq='6H') location = Location(latitude=32, longitude=-111) - solar_position = location.get_solarposition(times) + return location.get_solarposition(times) + + +def test_PVSystem_get_irradiance(solar_pos): + system = pvsystem.PVSystem(surface_tilt=32, surface_azimuth=135) irrads = pd.DataFrame({'dni':[900,0], 'ghi':[600,0], 'dhi':[100,0]}, - index=times) + index=solar_pos.index) - irradiance = system.get_irradiance(solar_position['apparent_zenith'], - solar_position['azimuth'], + irradiance = system.get_irradiance(solar_pos['apparent_zenith'], + solar_pos['azimuth'], irrads['dni'], irrads['ghi'], irrads['dhi']) expected = pd.DataFrame(data=np.array( - [[ 883.65494055, 745.86141676, 137.79352379, 126.397131 , - 11.39639279], - [ 0. , -0. , 0. , 0. , 0. ]]), + [[883.65494055, 745.86141676, 137.79352379, 126.397131, 11.39639279], + [0., -0., 0., 0., 0.]]), columns=['poa_global', 'poa_direct', 'poa_diffuse', 'poa_sky_diffuse', 'poa_ground_diffuse'], - index=times) + index=solar_pos.index) + assert_frame_equal(irradiance, expected, check_less_precise=2) + +def test_PVSystem_get_irradiance_albedo(solar_pos): + system = pvsystem.PVSystem(surface_tilt=32, surface_azimuth=135) + irrads = pd.DataFrame({'dni': [900, 0], 'ghi': [600, 0], 'dhi': [100, 0], + 'albedo': [0.5, 0.5]}, + index=solar_pos.index) + # albedo as a Series + irradiance = system.get_irradiance(solar_pos['apparent_zenith'], + solar_pos['azimuth'], + irrads['dni'], + irrads['ghi'], + irrads['dhi'], + albedo=irrads['albedo']) + expected = pd.DataFrame(data=np.array( + [[895.05134334, 745.86141676, 149.18992658, 126.397131, 22.79279558], + [0., -0., 0., 0., 0.]]), + columns=['poa_global', 'poa_direct', 'poa_diffuse', 'poa_sky_diffuse', + 'poa_ground_diffuse'], + index=solar_pos.index) assert_frame_equal(irradiance, expected, check_less_precise=2) -def test_PVSystem_get_irradiance_model(mocker): +def test_PVSystem_get_irradiance_model(mocker, solar_pos): spy_perez = mocker.spy(irradiance, 'perez') spy_haydavies = mocker.spy(irradiance, 'haydavies') system = pvsystem.PVSystem(surface_tilt=32, surface_azimuth=135) - times = pd.date_range(start='20160101 1200-0700', - end='20160101 1800-0700', freq='6H') - location = Location(latitude=32, longitude=-111) - solar_position = location.get_solarposition(times) irrads = pd.DataFrame({'dni': [900, 0], 'ghi': [600, 0], 'dhi': [100, 0]}, - index=times) - system.get_irradiance(solar_position['apparent_zenith'], - solar_position['azimuth'], + index=solar_pos.index) + system.get_irradiance(solar_pos['apparent_zenith'], + solar_pos['azimuth'], irrads['dni'], irrads['ghi'], irrads['dhi']) spy_haydavies.assert_called_once() - system.get_irradiance(solar_position['apparent_zenith'], - solar_position['azimuth'], + system.get_irradiance(solar_pos['apparent_zenith'], + solar_pos['azimuth'], irrads['dni'], irrads['ghi'], irrads['dhi'], @@ -1725,31 +1745,28 @@ def test_PVSystem_get_irradiance_model(mocker): spy_perez.assert_called_once() -def test_PVSystem_multi_array_get_irradiance(): +def test_PVSystem_multi_array_get_irradiance(solar_pos): array_one = pvsystem.Array(pvsystem.FixedMount(surface_tilt=32, surface_azimuth=135)) array_two = pvsystem.Array(pvsystem.FixedMount(surface_tilt=5, surface_azimuth=150)) system = pvsystem.PVSystem(arrays=[array_one, array_two]) - location = Location(latitude=32, longitude=-111) - times = pd.date_range(start='20160101 1200-0700', - end='20160101 1800-0700', freq='6H') - solar_position = location.get_solarposition(times) + irrads = pd.DataFrame({'dni': [900, 0], 'ghi': [600, 0], 'dhi': [100, 0]}, - index=times) + index=solar_pos.index) array_one_expected = array_one.get_irradiance( - solar_position['apparent_zenith'], - solar_position['azimuth'], + solar_pos['apparent_zenith'], + solar_pos['azimuth'], irrads['dni'], irrads['ghi'], irrads['dhi'] ) array_two_expected = array_two.get_irradiance( - solar_position['apparent_zenith'], - solar_position['azimuth'], + solar_pos['apparent_zenith'], + solar_pos['azimuth'], irrads['dni'], irrads['ghi'], irrads['dhi'] ) array_one_irrad, array_two_irrad = system.get_irradiance( - solar_position['apparent_zenith'], - solar_position['azimuth'], + solar_pos['apparent_zenith'], + solar_pos['azimuth'], irrads['dni'], irrads['ghi'], irrads['dhi'] ) assert_frame_equal( @@ -1760,7 +1777,7 @@ def test_PVSystem_multi_array_get_irradiance(): ) -def test_PVSystem_multi_array_get_irradiance_multi_irrad(): +def test_PVSystem_multi_array_get_irradiance_multi_irrad(solar_pos): """Test a system with two identical arrays but different irradiance. Because only the irradiance is different we expect the same output @@ -1771,39 +1788,36 @@ def test_PVSystem_multi_array_get_irradiance_multi_irrad(): array_one = pvsystem.Array(pvsystem.FixedMount(0, 180)) array_two = pvsystem.Array(pvsystem.FixedMount(0, 180)) system = pvsystem.PVSystem(arrays=[array_one, array_two]) - location = Location(latitude=32, longitude=-111) - times = pd.date_range(start='20160101 1200-0700', - end='20160101 1800-0700', freq='6H') - solar_position = location.get_solarposition(times) + irrads = pd.DataFrame({'dni': [900, 0], 'ghi': [600, 0], 'dhi': [100, 0]}, - index=times) + index=solar_pos.index) irrads_two = pd.DataFrame( {'dni': [0, 900], 'ghi': [0, 600], 'dhi': [0, 100]}, - index=times + index=solar_pos.index ) array_irrad = system.get_irradiance( - solar_position['apparent_zenith'], - solar_position['azimuth'], + solar_pos['apparent_zenith'], + solar_pos['azimuth'], (irrads['dhi'], irrads['dhi']), (irrads['ghi'], irrads['ghi']), (irrads['dni'], irrads['dni']) ) assert_frame_equal(array_irrad[0], array_irrad[1]) array_irrad = system.get_irradiance( - solar_position['apparent_zenith'], - solar_position['azimuth'], + solar_pos['apparent_zenith'], + solar_pos['azimuth'], (irrads['dhi'], irrads_two['dhi']), (irrads['ghi'], irrads_two['ghi']), (irrads['dni'], irrads_two['dni']) ) array_one_expected = array_one.get_irradiance( - solar_position['apparent_zenith'], - solar_position['azimuth'], + solar_pos['apparent_zenith'], + solar_pos['azimuth'], irrads['dhi'], irrads['ghi'], irrads['dni'] ) array_two_expected = array_two.get_irradiance( - solar_position['apparent_zenith'], - solar_position['azimuth'], + solar_pos['apparent_zenith'], + solar_pos['azimuth'], irrads_two['dhi'], irrads_two['ghi'], irrads_two['dni'] ) assert not array_irrad[0].equals(array_irrad[1]) @@ -1812,15 +1826,15 @@ def test_PVSystem_multi_array_get_irradiance_multi_irrad(): with pytest.raises(ValueError, match="Length mismatch for per-array parameter"): system.get_irradiance( - solar_position['apparent_zenith'], - solar_position['azimuth'], + solar_pos['apparent_zenith'], + solar_pos['azimuth'], (irrads['dhi'], irrads_two['dhi'], irrads['dhi']), (irrads['ghi'], irrads_two['ghi']), irrads['dni'] ) array_irrad = system.get_irradiance( - solar_position['apparent_zenith'], - solar_position['azimuth'], + solar_pos['apparent_zenith'], + solar_pos['azimuth'], (irrads['dhi'], irrads_two['dhi']), irrads['ghi'], irrads['dni'] @@ -1829,6 +1843,44 @@ def test_PVSystem_multi_array_get_irradiance_multi_irrad(): assert not array_irrad[0].equals(array_irrad[1]) +def test_Array_get_irradiance(solar_pos): + array = pvsystem.Array(pvsystem.FixedMount(surface_tilt=32, + surface_azimuth=135)) + irrads = pd.DataFrame({'dni': [900, 0], 'ghi': [600, 0], 'dhi': [100, 0]}, + index=solar_pos.index) + # defaults for kwargs + modeled = array.get_irradiance( + solar_pos['apparent_zenith'], + solar_pos['azimuth'], + irrads['dni'], irrads['ghi'], irrads['dhi'] + ) + expected = pd.DataFrame( + data=np.array( + [[883.65494055, 745.86141676, 137.79352379, 126.397131, + 11.39639279], + [0., -0., 0., 0., 0.]]), + columns=['poa_global', 'poa_direct', 'poa_diffuse', 'poa_sky_diffuse', + 'poa_ground_diffuse'], + index=solar_pos.index + ) + assert_frame_equal(modeled, expected, check_less_precise=5) + # with specified kwargs, use isotropic sky diffuse because it's easier + modeled = array.get_irradiance( + solar_pos['apparent_zenith'], + solar_pos['azimuth'], + irrads['dni'], irrads['ghi'], irrads['dhi'], + albedo=0.5, model='isotropic' + ) + sky_diffuse = irradiance.isotropic(array.mount.surface_tilt, irrads['dhi']) + ground_diff = irradiance.get_ground_diffuse( + array.mount.surface_tilt, irrads['ghi'], 0.5, surface_type=None) + aoi = irradiance.aoi(array.mount.surface_tilt, array.mount.surface_azimuth, + solar_pos['apparent_zenith'], solar_pos['azimuth']) + direct = irrads['dni'] * cosd(aoi) + expected = sky_diffuse + ground_diff + direct + assert_series_equal(expected, expected, check_less_precise=5) + + @fail_on_pvlib_version('0.10') @pytest.mark.parametrize('attr', ['module_parameters', 'module', 'module_type', 'temperature_model_parameters', 'albedo', diff --git a/pvlib/tests/test_tracking.py b/pvlib/tests/test_tracking.py index 0fbace0f17..87452939f5 100644 --- a/pvlib/tests/test_tracking.py +++ b/pvlib/tests/test_tracking.py @@ -393,6 +393,25 @@ def test_get_irradiance(): assert_frame_equal(irradiance, expected, check_less_precise=2) + # test with albedo as a Series + irrads['albedo'] = [0.5, 0.5] + with np.errstate(invalid='ignore'): + irradiance = system.get_irradiance(tracker_data['surface_tilt'], + tracker_data['surface_azimuth'], + solar_zenith, + solar_azimuth, + irrads['dni'], + irrads['ghi'], + irrads['dhi'], + albedo=irrads['albedo']) + + expected = pd.Series(data=[21.05514984, nan], index=times, + name='poa_ground_diffuse') + + assert_series_equal(irradiance['poa_ground_diffuse'], expected, + check_less_precise=2) + + def test_SingleAxisTracker___repr__(): with pytest.warns(pvlibDeprecationWarning): diff --git a/pvlib/tracking.py b/pvlib/tracking.py index 9bd216207d..7afb46f253 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -188,7 +188,8 @@ def get_aoi(self, surface_tilt, surface_azimuth, solar_zenith, @_unwrap_single_value def get_irradiance(self, surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, dni, ghi, dhi, - dni_extra=None, airmass=None, model='haydavies', + albedo=None, dni_extra=None, airmass=None, + model='haydavies', **kwargs): """ Uses the :func:`irradiance.get_total_irradiance` function to @@ -215,6 +216,8 @@ def get_irradiance(self, surface_tilt, surface_azimuth, Global horizontal irradiance dhi : float or Series Diffuse horizontal irradiance + albedo : None, float or Series, default None + Ground surface albedo. [unitless] dni_extra : float or Series, default None Extraterrestrial direct normal irradiance airmass : float or Series, default None @@ -245,6 +248,13 @@ def get_irradiance(self, surface_tilt, surface_azimuth, ghi = self._validate_per_array(ghi, system_wide=True) dhi = self._validate_per_array(dhi, system_wide=True) + if albedo is None: + # assign default albedo here because SingleAxisTracker + # initializes albedo to None + albedo = 0.25 + + albedo = self._validate_per_array(albedo, system_wide=True) + return tuple( irradiance.get_total_irradiance( surface_tilt, @@ -255,10 +265,10 @@ def get_irradiance(self, surface_tilt, surface_azimuth, dni_extra=dni_extra, airmass=airmass, model=model, - albedo=self.arrays[0].albedo, + albedo=albedo, **kwargs) - for array, dni, ghi, dhi in zip( - self.arrays, dni, ghi, dhi + for array, dni, ghi, dhi, albedo in zip( + self.arrays, dni, ghi, dhi, albedo ) )