diff --git a/docs/sphinx/source/reference/irradiance.rst b/docs/sphinx/source/reference/irradiance.rst index e0a5777533..ad3da96eab 100644 --- a/docs/sphinx/source/reference/irradiance.rst +++ b/docs/sphinx/source/reference/irradiance.rst @@ -28,6 +28,7 @@ Decomposing and combining irradiance irradiance.poa_components irradiance.get_ground_diffuse irradiance.dni + irradiance.complete_irradiance Transposition models -------------------- diff --git a/docs/sphinx/source/whatsnew/v0.9.4.rst b/docs/sphinx/source/whatsnew/v0.9.4.rst index ae6b710bed..00bc89207a 100644 --- a/docs/sphinx/source/whatsnew/v0.9.4.rst +++ b/docs/sphinx/source/whatsnew/v0.9.4.rst @@ -10,14 +10,17 @@ Deprecations Enhancements ~~~~~~~~~~~~ * Multiple code style issues fixed that were reported by LGTM analysis. (:issue:`1275`, :pull:`1559`) +* Added a function to calculate one of GHI, DHI, and DNI from values of the other two. + :py:func:`~pvlib.irradiance.complete_irradiance` + (:issue:`1565`, :pull:`1567`) * Add optional ``return_components`` parameter to :py:func:`pvlib.irradiance.haydavies` to return individual diffuse irradiance components (:issue:`1553`, :pull:`1568`) + Bug fixes ~~~~~~~~~ - Testing ~~~~~~~ * Corrected a flawed test for :py:func:`~pvlib.irradiance.get_ground_diffuse` (:issue:`1569`, :pull:`1575`) @@ -36,6 +39,7 @@ Requirements Contributors ~~~~~~~~~~~~ +* Kirsten Perry (:ghuser:`kperrynrel`) * Christian Orner (:ghuser:`chrisorner`) * Saurabh Aneja (:ghuser:`spaneja`) * Marcus Boumans (:ghuser:`bowie2211`) diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py index ddcb23a7ff..f6d166d44c 100644 --- a/pvlib/irradiance.py +++ b/pvlib/irradiance.py @@ -12,6 +12,7 @@ import pandas as pd from pvlib import atmosphere, solarposition, tools +import pvlib # used to avoid dni name collision in complete_irradiance # see References section of get_ground_diffuse function @@ -2948,3 +2949,67 @@ def dni(ghi, dhi, zenith, clearsky_dni=None, clearsky_tolerance=1.1, (zenith < zenith_threshold_for_zero_dni) & (dni > max_dni)] = max_dni return dni + + +def complete_irradiance(solar_zenith, + ghi=None, + dhi=None, + dni=None, + dni_clear=None): + r""" + Use the component sum equations to calculate the missing series, using + the other available time series. One of the three parameters (ghi, dhi, + dni) is passed as None, and the other associated series passed are used to + calculate the missing series value. + + The "component sum" or "closure" equation relates the three + primary irradiance components as follows: + + .. math:: + + GHI = DHI + DNI \cos(\theta_z) + + Parameters + ---------- + solar_zenith : Series + Zenith angles in decimal degrees, with datetime index. + Angles must be >=0 and <=180. Must have the same datetime index + as ghi, dhi, and dni series, when available. + ghi : Series, optional + Pandas series of dni data, with datetime index. Must have the same + datetime index as dni, dhi, and zenith series, when available. + dhi : Series, optional + Pandas series of dni data, with datetime index. Must have the same + datetime index as ghi, dni, and zenith series, when available. + dni : Series, optional + Pandas series of dni data, with datetime index. Must have the same + datetime index as ghi, dhi, and zenith series, when available. + dni_clear : Series, optional + Pandas series of clearsky dni data. Must have the same datetime index + as ghi, dhi, dni, and zenith series, when available. See + :py:func:`dni` for details. + + Returns + ------- + component_sum_df : Dataframe + Pandas series of 'ghi', 'dhi', and 'dni' columns with datetime index + """ + if ghi is not None and dhi is not None and dni is None: + dni = pvlib.irradiance.dni(ghi, dhi, solar_zenith, + clearsky_dni=dni_clear, + clearsky_tolerance=1.1) + elif dni is not None and dhi is not None and ghi is None: + ghi = (dhi + dni * tools.cosd(solar_zenith)) + elif dni is not None and ghi is not None and dhi is None: + dhi = (ghi - dni * tools.cosd(solar_zenith)) + else: + raise ValueError( + "Please check that exactly one of ghi, dhi and dni parameters " + "is set to None" + ) + # Merge the outputs into a master dataframe containing 'ghi', 'dhi', + # and 'dni' columns + component_sum_df = pd.DataFrame({'ghi': ghi, + 'dhi': dhi, + 'dni': dni}) + return component_sum_df diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 8211981433..ccf2e614f3 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1289,13 +1289,13 @@ def complete_irradiance(self, weather): self._assign_times() self.results.solar_position = self.location.get_solarposition( self.results.times, method=self.solar_position_method) - + # Calculate the irradiance using the component sum equations, + # if needed if isinstance(weather, tuple): for w in self.results.weather: self._complete_irradiance(w) else: self._complete_irradiance(self.results.weather) - return self def _complete_irradiance(self, weather): @@ -1304,26 +1304,32 @@ def _complete_irradiance(self, weather): "Results can be too high or negative.\n" + "Help to improve this function on github:\n" + "https://github.com/pvlib/pvlib-python \n") - if {'ghi', 'dhi'} <= icolumns and 'dni' not in icolumns: clearsky = self.location.get_clearsky( weather.index, solar_position=self.results.solar_position) - weather.loc[:, 'dni'] = pvlib.irradiance.dni( - weather.loc[:, 'ghi'], weather.loc[:, 'dhi'], - self.results.solar_position.zenith, - clearsky_dni=clearsky['dni'], - clearsky_tolerance=1.1) + complete_irrad_df = pvlib.irradiance.complete_irradiance( + solar_zenith=self.results.solar_position.zenith, + ghi=weather.ghi, + dhi=weather.dhi, + dni=None, + dni_clear=clearsky.dni) + weather.loc[:, 'dni'] = complete_irrad_df.dni elif {'dni', 'dhi'} <= icolumns and 'ghi' not in icolumns: warnings.warn(wrn_txt, UserWarning) - weather.loc[:, 'ghi'] = ( - weather.dhi + weather.dni * - tools.cosd(self.results.solar_position.zenith) - ) + complete_irrad_df = pvlib.irradiance.complete_irradiance( + solar_zenith=self.results.solar_position.zenith, + ghi=None, + dhi=weather.dhi, + dni=weather.dni) + weather.loc[:, 'ghi'] = complete_irrad_df.ghi elif {'dni', 'ghi'} <= icolumns and 'dhi' not in icolumns: warnings.warn(wrn_txt, UserWarning) - weather.loc[:, 'dhi'] = ( - weather.ghi - weather.dni * - tools.cosd(self.results.solar_position.zenith)) + complete_irrad_df = pvlib.irradiance.complete_irradiance( + solar_zenith=self.results.solar_position.zenith, + ghi=weather.ghi, + dhi=None, + dni=weather.dni) + weather.loc[:, 'dhi'] = complete_irrad_df.dhi def _prep_inputs_solar_pos(self, weather): """ diff --git a/pvlib/tests/test_irradiance.py b/pvlib/tests/test_irradiance.py index c87e8dcb47..ae3a7ec88a 100644 --- a/pvlib/tests/test_irradiance.py +++ b/pvlib/tests/test_irradiance.py @@ -7,8 +7,8 @@ import pandas as pd import pytest -from numpy.testing import assert_almost_equal, assert_allclose - +from numpy.testing import (assert_almost_equal, + assert_allclose) from pvlib import irradiance from .conftest import ( @@ -34,23 +34,23 @@ def times(): @pytest.fixture def irrad_data(times): return pd.DataFrame(np.array( - [[ 0. , 0. , 0. ], - [ 79.73860422, 316.1949056 , 40.46149818], + [[0., 0., 0.], + [79.73860422, 316.1949056, 40.46149818], [1042.48031487, 939.95469881, 118.45831879], - [ 257.20751138, 646.22886049, 62.03376265]]), + [257.20751138, 646.22886049, 62.03376265]]), columns=['ghi', 'dni', 'dhi'], index=times) @pytest.fixture def ephem_data(times): return pd.DataFrame(np.array( - [[124.0390863 , 124.0390863 , -34.0390863 , -34.0390863 , + [[124.0390863, 124.0390863, -34.0390863, -34.0390863, 352.69550699, -2.36677158], - [ 82.85457044, 82.97705621, 7.14542956, 7.02294379, - 66.71410338, -2.42072165], - [ 10.56413562, 10.56725766, 79.43586438, 79.43274234, + [82.85457044, 82.97705621, 7.14542956, 7.02294379, + 66.71410338, -2.42072165], + [10.56413562, 10.56725766, 79.43586438, 79.43274234, 144.76567754, -2.47457321], - [ 72.41687122, 72.46903556, 17.58312878, 17.53096444, + [72.41687122, 72.46903556, 17.58312878, 17.53096444, 287.04104128, -2.52831909]]), columns=['apparent_zenith', 'zenith', 'apparent_elevation', 'elevation', 'azimuth', 'equation_of_time'], @@ -267,7 +267,7 @@ def test_perez(irrad_data, ephem_data, dni_et, relative_airmass): dni_et, ephem_data['apparent_zenith'], ephem_data['azimuth'], relative_airmass) expected = pd.Series(np.array( - [ 0. , 31.46046871, np.nan, 45.45539877]), + [0., 31.46046871, np.nan, 45.45539877]), index=irrad_data.index) assert_series_equal(out, expected, check_less_precise=2) @@ -280,10 +280,10 @@ def test_perez_components(irrad_data, ephem_data, dni_et, relative_airmass): ephem_data['azimuth'], relative_airmass, return_components=True) expected = pd.DataFrame(np.array( - [[ 0. , 31.46046871, np.nan, 45.45539877], - [ 0. , 26.84138589, np.nan, 31.72696071], - [ 0. , 0. , np.nan, 4.47966439], - [ 0. , 4.62212181, np.nan, 9.25316454]]).T, + [[0., 31.46046871, np.nan, 45.45539877], + [0., 26.84138589, np.nan, 31.72696071], + [0., 0., np.nan, 4.47966439], + [0., 4.62212181, np.nan, 9.25316454]]).T, columns=['sky_diffuse', 'isotropic', 'circumsolar', 'horizon'], index=irrad_data.index ) @@ -306,12 +306,12 @@ def test_perez_negative_horizon(): # dni_e is slightly rounded from irradiance.get_extra_radiation # airmass from atmosphere.get_relative_airmas inputs = pd.DataFrame(np.array( - [[ 158, 19, 1, 0, 0], - [ 249, 165, 136, 93, 50], - [ 57.746951, 57.564205, 60.813841, 66.989435, 75.353368], - [ 171.003315, 187.346924, 202.974357, 216.725599, 228.317233], + [[158, 19, 1, 0, 0], + [249, 165, 136, 93, 50], + [57.746951, 57.564205, 60.813841, 66.989435, 75.353368], + [171.003315, 187.346924, 202.974357, 216.725599, 228.317233], [1414, 1414, 1414, 1414, 1414], - [ 1.869315, 1.859981, 2.044429, 2.544943, 3.900136]]).T, + [1.869315, 1.859981, 2.044429, 2.544943, 3.900136]]).T, columns=['dni', 'dhi', 'solar_zenith', 'solar_azimuth', 'dni_extra', 'airmass'], index=times @@ -329,7 +329,7 @@ def test_perez_negative_horizon(): [[281.410185, 152.20879, 123.867898, 82.836412, 43.517015], [166.785419, 142.24475, 119.173875, 83.525150, 45.725931], [113.548755, 16.09757, 9.956174, 3.142467, 0], - [ 1.076010, -6.13353, -5.262151, -3.831230, -2.208923]]).T, + [1.076010, -6.13353, -5.262151, -3.831230, -2.208923]]).T, columns=['sky_diffuse', 'isotropic', 'circumsolar', 'horizon'], index=times ) @@ -350,7 +350,7 @@ def test_perez_arrays(irrad_data, ephem_data, dni_et, relative_airmass): ephem_data['azimuth'].values, relative_airmass.values) expected = np.array( - [ 0. , 31.46046871, np.nan, 45.45539877]) + [0., 31.46046871, np.nan, 45.45539877]) assert_allclose(out, expected, atol=1e-2) assert isinstance(out, np.ndarray) @@ -508,14 +508,14 @@ def test_poa_components(irrad_data, ephem_data, dni_et, relative_airmass): out = irradiance.poa_components( aoi, irrad_data['dni'], diff_perez, gr_sand) expected = pd.DataFrame(np.array( - [[ 0. , -0. , 0. , 0. , - 0. ], - [ 35.19456561, 0. , 35.19456561, 31.4635077 , + [[0., -0., 0., 0., + 0.], + [35.19456561, 0., 35.19456561, 31.4635077, 3.73105791], [956.18253696, 798.31939281, 157.86314414, 109.08433162, - 48.77881252], - [ 90.99624896, 33.50143401, 57.49481495, 45.45978964, - 12.03502531]]), + 48.77881252], + [90.99624896, 33.50143401, 57.49481495, 45.45978964, + 12.03502531]]), columns=['poa_global', 'poa_direct', 'poa_diffuse', 'poa_sky_diffuse', 'poa_ground_diffuse'], index=irrad_data.index) @@ -720,9 +720,9 @@ def test_gti_dirint(): expected_col_order = ['ghi', 'dni', 'dhi'] expected = pd.DataFrame(array( - [[ 21.05796198, 0. , 21.05796198], - [ 291.40037163, 63.41290679, 246.56067523], - [ 931.04078010, 695.94965324, 277.06172442]]), + [[21.05796198, 0., 21.05796198], + [291.40037163, 63.41290679, 246.56067523], + [931.04078010, 695.94965324, 277.06172442]]), columns=expected_col_order, index=times) assert_frame_equal(output, expected) @@ -744,9 +744,9 @@ def test_gti_dirint(): pressure=pressure) expected = pd.DataFrame(array( - [[ 21.05796198, 0. , 21.05796198], - [ 293.21310935, 63.27500913, 248.47092131], - [ 932.46756378, 648.05001357, 323.49974813]]), + [[21.05796198, 0., 21.05796198], + [293.21310935, 63.27500913, 248.47092131], + [932.46756378, 648.05001357, 323.49974813]]), columns=expected_col_order, index=times) assert_frame_equal(output, expected) @@ -758,9 +758,9 @@ def test_gti_dirint(): albedo=albedo) expected = pd.DataFrame(array( - [[ 21.3592591, 0. , 21.3592591 ], - [ 294.4985420, 66.25848451, 247.64671830], - [ 941.7943404, 727.50552952, 258.16276278]]), + [[21.3592591, 0., 21.3592591], + [294.4985420, 66.25848451, 247.64671830], + [941.7943404, 727.50552952, 258.16276278]]), columns=expected_col_order, index=times) assert_frame_equal(output, expected) @@ -780,9 +780,9 @@ def test_gti_dirint(): temp_dew=temp_dew) expected = pd.DataFrame(array( - [[ 21.05796198, 0., 21.05796198], - [ 295.06070190, 38.20346345, 268.0467738], - [ 931.79627208, 689.81549269, 283.5817439]]), + [[21.05796198, 0., 21.05796198], + [295.06070190, 38.20346345, 268.0467738], + [931.79627208, 689.81549269, 283.5817439]]), columns=expected_col_order, index=times) assert_frame_equal(output, expected) @@ -989,7 +989,7 @@ def airmass_kt(): def test_kt_kt_prime_factor(airmass_kt): out = irradiance._kt_kt_prime_factor(airmass_kt) - expected = np.array([ 0.999971, 0.723088, 0.548811, 0.471068]) + expected = np.array([0.999971, 0.723088, 0.548811, 0.471068]) assert_allclose(out, expected, atol=1e-5) @@ -1000,11 +1000,11 @@ def test_clearsky_index(): with np.errstate(invalid='ignore', divide='ignore'): out = irradiance.clearsky_index(ghi_measured, ghi_modeled) expected = np.array( - [[1. , 0. , 0. , 0. , 0. , np.nan], - [0. , 0. , 0. , 0. , 0. , np.nan], - [0. , 0. , 1. , 2. , 2. , np.nan], - [0. , 0. , 0.002 , 1. , 2. , np.nan], - [0. , 0. , 0.001 , 0.5 , 1. , np.nan], + [[1., 0., 0., 0., 0., np.nan], + [0., 0., 0., 0., 0., np.nan], + [0., 0., 1., 2., 2., np.nan], + [0., 0., 0.002, 1., 2., np.nan], + [0., 0., 0.001, 0.5, 1., np.nan], [np.nan, np.nan, np.nan, np.nan, np.nan, np.nan]]) assert_allclose(out, expected, atol=0.001) # specify max_clearsky_index @@ -1012,11 +1012,11 @@ def test_clearsky_index(): out = irradiance.clearsky_index(ghi_measured, ghi_modeled, max_clearsky_index=1.5) expected = np.array( - [[1. , 0. , 0. , 0. , 0. , np.nan], - [0. , 0. , 0. , 0. , 0. , np.nan], - [0. , 0. , 1. , 1.5 , 1.5 , np.nan], - [0. , 0. , 0.002 , 1. , 1.5 , np.nan], - [0. , 0. , 0.001 , 0.5 , 1. , np.nan], + [[1., 0., 0., 0., 0., np.nan], + [0., 0., 0., 0., 0., np.nan], + [0., 0., 1., 1.5, 1.5, np.nan], + [0., 0., 0.002, 1., 1.5, np.nan], + [0., 0., 0.001, 0.5, 1., np.nan], [np.nan, np.nan, np.nan, np.nan, np.nan, np.nan]]) assert_allclose(out, expected, atol=0.001) # scalars @@ -1040,29 +1040,29 @@ def test_clearness_index(): out = irradiance.clearness_index(ghi, solar_zenith, 1370) # np.set_printoptions(precision=3, floatmode='maxprec', suppress=True) expected = np.array( - [[0. , 0. , 0.011, 2. ], - [0. , 0. , 0.011, 2. ], - [0. , 0. , 0.011, 2. ], - [0. , 0. , 0.001, 0.73 ]]) + [[0., 0., 0.011, 2.], + [0., 0., 0.011, 2.], + [0., 0., 0.011, 2.], + [0., 0., 0.001, 0.73]]) assert_allclose(out, expected, atol=0.001) # specify min_cos_zenith with np.errstate(invalid='ignore', divide='ignore'): out = irradiance.clearness_index(ghi, solar_zenith, 1400, min_cos_zenith=0) expected = np.array( - [[0. , nan, 2. , 2. ], - [0. , 0. , 2. , 2. ], - [0. , 0. , 2. , 2. ], - [0. , 0. , 0.001, 0.714]]) + [[0., nan, 2., 2.], + [0., 0., 2., 2.], + [0., 0., 2., 2.], + [0., 0., 0.001, 0.714]]) assert_allclose(out, expected, atol=0.001) # specify max_clearness_index out = irradiance.clearness_index(ghi, solar_zenith, 1370, max_clearness_index=0.82) expected = np.array( - [[ 0. , 0. , 0.011, 0.82 ], - [ 0. , 0. , 0.011, 0.82 ], - [ 0. , 0. , 0.011, 0.82 ], - [ 0. , 0. , 0.001, 0.73 ]]) + [[0., 0., 0.011, 0.82], + [0., 0., 0.011, 0.82], + [0., 0., 0.011, 0.82], + [0., 0., 0.001, 0.73]]) assert_allclose(out, expected, atol=0.001) # specify min_cos_zenith and max_clearness_index with np.errstate(invalid='ignore', divide='ignore'): @@ -1070,10 +1070,10 @@ def test_clearness_index(): min_cos_zenith=0, max_clearness_index=0.82) expected = np.array( - [[ 0. , nan, 0.82 , 0.82 ], - [ 0. , 0. , 0.82 , 0.82 ], - [ 0. , 0. , 0.82 , 0.82 ], - [ 0. , 0. , 0.001, 0.714]]) + [[0., nan, 0.82, 0.82], + [0., 0., 0.82, 0.82], + [0., 0., 0.82, 0.82], + [0., 0., 0.001, 0.714]]) assert_allclose(out, expected, atol=0.001) # scalars out = irradiance.clearness_index(1000, 10, 1400) @@ -1095,19 +1095,19 @@ def test_clearness_index_zenith_independent(airmass_kt): out = irradiance.clearness_index_zenith_independent(clearness_index, airmass_kt) expected = np.array( - [[0. , 0. , 0.1 , 1. ], - [0. , 0. , 0.138, 1.383], - [0. , 0. , 0.182, 1.822], - [0. , 0. , 0.212, 2. ]]) + [[0., 0., 0.1, 1.], + [0., 0., 0.138, 1.383], + [0., 0., 0.182, 1.822], + [0., 0., 0.212, 2.]]) assert_allclose(out, expected, atol=0.001) # test max_clearness_index out = irradiance.clearness_index_zenith_independent( clearness_index, airmass_kt, max_clearness_index=0.82) expected = np.array( - [[ 0. , 0. , 0.1 , 0.82 ], - [ 0. , 0. , 0.138, 0.82 ], - [ 0. , 0. , 0.182, 0.82 ], - [ 0. , 0. , 0.212, 0.82 ]]) + [[0., 0., 0.1, 0.82], + [0., 0., 0.138, 0.82], + [0., 0., 0.182, 0.82], + [0., 0., 0.212, 0.82]]) assert_allclose(out, expected, atol=0.001) # scalars out = irradiance.clearness_index_zenith_independent(.4, 2) @@ -1121,3 +1121,69 @@ def test_clearness_index_zenith_independent(airmass_kt): airmass) expected = pd.Series([np.nan, 0.553744437562], index=times) assert_series_equal(out, expected) + + +def test_complete_irradiance(): + # Generate dataframe to test on + times = pd.date_range('2010-07-05 7:00:00-0700', periods=2, freq='H') + i = pd.DataFrame({'ghi': [372.103976116, 497.087579068], + 'dhi': [356.543700, 465.44400], + 'dni': [49.63565561689957, 62.10624908037814]}, + index=times) + # Define the solar position and clearsky dataframe + solar_position = pd.DataFrame({'apparent_zenith': [71.7303262449161, + 59.369], + 'zenith': [71.7764, 59.395]}, + index=pd.DatetimeIndex([ + '2010-07-05 07:00:00-0700', + '2010-07-05 08:00:00-0700'])) + clearsky = pd.DataFrame({'dni': [625.5254880160008, 778.7766443075865], + 'ghi': [246.3508023804681, 469.461381740857], + 'dhi': [50.25488725346631, 72.66909939636372]}, + index=pd.DatetimeIndex([ + '2010-07-05 07:00:00-0700', + '2010-07-05 08:00:00-0700'])) + # Test scenario where DNI is generated via component sum equation + complete_df = irradiance.complete_irradiance( + solar_position.apparent_zenith, + ghi=i.ghi, + dhi=i.dhi, + dni=None, + dni_clear=clearsky.dni) + # Assert that the ghi, dhi, and dni series match the original dataframe + # values + assert_frame_equal(complete_df, i) + # Test scenario where GHI is generated via component sum equation + complete_df = irradiance.complete_irradiance( + solar_position.apparent_zenith, + ghi=None, + dhi=i.dhi, + dni=i.dni, + dni_clear=clearsky.dni) + # Assert that the ghi, dhi, and dni series match the original dataframe + # values + assert_frame_equal(complete_df, i) + # Test scenario where DHI is generated via component sum equation + complete_df = irradiance.complete_irradiance( + solar_position.apparent_zenith, + ghi=i.ghi, + dhi=None, + dni=i.dni, + dni_clear=clearsky.dni) + # Assert that the ghi, dhi, and dni series match the original dataframe + # values + assert_frame_equal(complete_df, i) + # Test scenario where all parameters are passed (throw error) + with pytest.raises(ValueError): + irradiance.complete_irradiance(solar_position.apparent_zenith, + ghi=i.ghi, + dhi=i.dhi, + dni=i.dni, + dni_clear=clearsky.dni) + # Test scenario where only one parameter is passed (throw error) + with pytest.raises(ValueError): + irradiance.complete_irradiance(solar_position.apparent_zenith, + ghi=None, + dhi=None, + dni=i.dni, + dni_clear=clearsky.dni)