diff --git a/docs/sphinx/source/whatsnew/v0.6.0.rst b/docs/sphinx/source/whatsnew/v0.6.0.rst index f3cb70a46a..58eb5648b7 100644 --- a/docs/sphinx/source/whatsnew/v0.6.0.rst +++ b/docs/sphinx/source/whatsnew/v0.6.0.rst @@ -11,6 +11,12 @@ API Changes support from PVSystem.pvwatts_losses. Enables custom losses specification in ModelChain calculations. (:issue:`484`) * removed irradiance parameter from ModelChain.run_model and ModelChain.prepare_inputs +* Add ``perez_enhancement`` keyword argument to clearsky.ineichen to control + whether or not the "perez enhancement factor" is applied. The enhancement + factor was always applied until now. Now it is turned off by default. The + enhancement factor can yield unphysical results, especially for latitudes + closer to the poles and especially in the winter months. It may yield + improved results under other conditions. (:issue:`435`) Enhancements @@ -81,8 +87,11 @@ Documentation Testing ~~~~~~~ * Add pytest-mock dependency -* Use pytest-mock to ensure that PVSystem methods call corresponding functions - correctly. Removes implicit dependence on precise return values of functions +* Use pytest-mock to ensure that PVSystem and ModelChain methods call + corresponding functions correctly. Removes implicit dependence on precise + return values of some function/methods. (:issue:`394`) +* Additional test refactoring to limit test result dependence to a single + function per test. (:issue:`394`) * Use pytest-mock to ensure that ModelChain DC model is set up correctly. diff --git a/pvlib/clearsky.py b/pvlib/clearsky.py index 75f598baeb..8ce301008f 100644 --- a/pvlib/clearsky.py +++ b/pvlib/clearsky.py @@ -16,7 +16,7 @@ def ineichen(apparent_zenith, airmass_absolute, linke_turbidity, - altitude=0, dni_extra=1364.): + altitude=0, dni_extra=1364., perez_enhancement=False): ''' Determine clear sky GHI, DNI, and DHI from Ineichen/Perez model. @@ -47,6 +47,12 @@ def ineichen(apparent_zenith, airmass_absolute, linke_turbidity, Extraterrestrial irradiance. The units of ``dni_extra`` determine the units of the output. + perez_enhancement : bool, default False + Controls if the Perez enhancement factor should be applied. + Setting to True may produce spurious results for times when + the Sun is near the horizon and the airmass is high. + See https://github.com/pvlib/pvlib-python/issues/435 + Returns ------- clearsky : DataFrame (if Series input) or OrderedDict of arrays @@ -79,28 +85,9 @@ def ineichen(apparent_zenith, airmass_absolute, linke_turbidity, ISES Solar World Congress, June 2003. Goteborg, Sweden. ''' - # Dan's note on the TL correction: By my reading of the publication - # on pages 151-157, Ineichen and Perez introduce (among other - # things) three things. 1) Beam model in eqn. 8, 2) new turbidity - # factor in eqn 9 and appendix A, and 3) Global horizontal model in - # eqn. 11. They do NOT appear to use the new turbidity factor (item - # 2 above) in either the beam or GHI models. The phrasing of - # appendix A seems as if there are two separate corrections, the - # first correction is used to correct the beam/GHI models, and the - # second correction is used to correct the revised turibidity - # factor. In my estimation, there is no need to correct the - # turbidity factor used in the beam/GHI models. - - # Create the corrected TL for TL < 2 - # TLcorr = TL; - # TLcorr(TL < 2) = TLcorr(TL < 2) - 0.25 .* (2-TLcorr(TL < 2)) .^ (0.5); - - # This equation is found in Solar Energy 73, pg 311. Full ref: Perez - # et. al., Vol. 73, pp. 307-317 (2002). It is slightly different - # than the equation given in Solar Energy 73, pg 156. We used the - # equation from pg 311 because of the existence of known typos in - # the pg 156 publication (notably the fh2-(TL-1) should be fh2 * - # (TL-1)). + # ghi is calculated using either the equations in [1] by setting + # perez_enhancement=False (default behavior) or using the model + # in [2] by setting perez_enhancement=True. # The NaN handling is a little subtle. The AM input is likely to # have NaNs that we'll want to map to 0s in the output. However, we @@ -119,8 +106,12 @@ def ineichen(apparent_zenith, airmass_absolute, linke_turbidity, cg1 = 5.09e-05 * altitude + 0.868 cg2 = 3.92e-05 * altitude + 0.0387 - ghi = (np.exp(-cg2*airmass_absolute*(fh1 + fh2*(tl - 1))) * - np.exp(0.01*airmass_absolute**1.8)) + ghi = np.exp(-cg2*airmass_absolute*(fh1 + fh2*(tl - 1))) + + # https://github.com/pvlib/pvlib-python/issues/435 + if perez_enhancement: + ghi *= np.exp(0.01*airmass_absolute**1.8) + # use fmax to map airmass nans to 0s. multiply and divide by tl to # reinsert tl nans ghi = cg1 * dni_extra * cos_zenith * tl / tl * np.fmax(ghi, 0) diff --git a/pvlib/location.py b/pvlib/location.py index e64ce52d53..bd47c27ef9 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -218,7 +218,7 @@ def get_clearsky(self, times, model='ineichen', solar_position=None, cs = clearsky.ineichen(apparent_zenith, airmass_absolute, linke_turbidity, altitude=self.altitude, - dni_extra=dni_extra) + dni_extra=dni_extra, **kwargs) elif model == 'haurwitz': cs = clearsky.haurwitz(apparent_zenith) elif model == 'simplified_solis': diff --git a/pvlib/test/test_clearsky.py b/pvlib/test/test_clearsky.py index e858cdf0d3..98007b449d 100644 --- a/pvlib/test/test_clearsky.py +++ b/pvlib/test/test_clearsky.py @@ -3,6 +3,7 @@ from collections import OrderedDict import numpy as np +from numpy import nan import pandas as pd import pytz @@ -20,35 +21,66 @@ def test_ineichen_series(): - tus = Location(32.2, -111, 'US/Arizona', 700) - times = pd.date_range(start='2014-06-24', end='2014-06-25', freq='3h') - times_localized = times.tz_localize(tus.tz) - ephem_data = solarposition.get_solarposition(times_localized, tus.latitude, - tus.longitude) - am = atmosphere.relativeairmass(ephem_data['apparent_zenith']) - am = atmosphere.absoluteairmass(am, atmosphere.alt2pres(tus.altitude)) + times = pd.date_range(start='2014-06-24', end='2014-06-25', freq='3h', + tz='America/Phoenix') + apparent_zenith = pd.Series(np.array( + [124.0390863, 113.38779941, 82.85457044, 46.0467599, 10.56413562, + 34.86074109, 72.41687122, 105.69538659, 124.05614124]), + index=times) + am = pd.Series(np.array( + [nan, nan, 6.97935524, 1.32355476, 0.93527685, + 1.12008114, 3.01614096, nan, nan]), + index=times) expected = pd.DataFrame(np. - array([[ 0. , 0. , 0. ], - [ 0. , 0. , 0. ], - [ 91.12492792, 321.16092181, 51.17628184], - [ 716.46580533, 888.90147035, 99.5050056 ], - [ 1053.42066043, 953.24925854, 116.32868969], - [ 863.54692781, 922.06124712, 106.95536561], - [ 271.06382274, 655.44925241, 73.05968071], - [ 0. , 0. , 0. ], - [ 0. , 0. , 0. ]]), + array([[ 0. , 0. , 0. ], + [ 0. , 0. , 0. ], + [ 65.49426624, 321.16092181, 25.54562017], + [ 704.6968125 , 888.90147035, 87.73601277], + [1044.1230677 , 953.24925854, 107.03109696], + [ 853.02065704, 922.06124712, 96.42909484], + [ 251.99427693, 655.44925241, 53.9901349 ], + [ 0. , 0. , 0. ], + [ 0. , 0. , 0. ]]), columns=['ghi', 'dni', 'dhi'], - index=times_localized) + index=times) - out = clearsky.ineichen(ephem_data['apparent_zenith'], am, 3) + out = clearsky.ineichen(apparent_zenith, am, 3) + assert_frame_equal(expected, out) + + +def test_ineichen_series_perez_enhancement(): + times = pd.date_range(start='2014-06-24', end='2014-06-25', freq='3h', + tz='America/Phoenix') + apparent_zenith = pd.Series(np.array( + [124.0390863, 113.38779941, 82.85457044, 46.0467599, 10.56413562, + 34.86074109, 72.41687122, 105.69538659, 124.05614124]), + index=times) + am = pd.Series(np.array( + [nan, nan, 6.97935524, 1.32355476, 0.93527685, + 1.12008114, 3.01614096, nan, nan]), + index=times) + expected = pd.DataFrame(np. + array([[ 0. , 0. , 0. ], + [ 0. , 0. , 0. ], + [ 91.1249279 , 321.16092171, 51.17628184], + [ 716.46580547, 888.9014706 , 99.50500553], + [1053.42066073, 953.24925905, 116.3286895 ], + [ 863.54692748, 922.06124652, 106.9553658 ], + [ 271.06382275, 655.44925213, 73.05968076], + [ 0. , 0. , 0. ], + [ 0. , 0. , 0. ]]), + columns=['ghi', 'dni', 'dhi'], + index=times) + + out = clearsky.ineichen(apparent_zenith, am, 3, perez_enhancement=True) assert_frame_equal(expected, out) def test_ineichen_scalar_input(): expected = OrderedDict() - expected['ghi'] = 1048.592893113678 + expected['ghi'] = 1038.159219 expected['dni'] = 942.2081860378344 - expected['dhi'] = 120.6989665520498 + expected['dhi'] = 110.26529293612793 out = clearsky.ineichen(10., 1., 3.) for k, v in expected.items(): @@ -74,9 +106,9 @@ def test_ineichen_nans(): expected['dni'] = np.full(length, np.nan) expected['dhi'] = np.full(length, np.nan) - expected['ghi'][length-1] = 1053.205472 - expected['dni'][length-1] = 946.352797 - expected['dhi'][length-1] = 121.2299 + expected['ghi'][length-1] = 1042.72590228 + expected['dni'][length-1] = 946.35279683 + expected['dhi'][length-1] = 110.75033088 out = clearsky.ineichen(apparent_zenith, airmass_absolute, linke_turbidity, dni_extra=dni_extra) @@ -89,43 +121,43 @@ def test_ineichen_arrays(): expected = OrderedDict() expected['ghi'] = (np. - array([[[ 1106.78342709, 1064.7691287 , 1024.34972343], - [ 847.84529406, 815.66047425, 784.69741345], - [ 192.19092519, 184.89521884, 177.87646277]], + array([[[1095.77074798, 1054.17449885, 1014.15727338], + [ 839.40909243, 807.54451692, 776.88954373], + [ 190.27859353, 183.05548067, 176.10656239]], - [[ 959.12310134, 775.2374976 , 626.60692548], - [ 734.73092205, 593.86637713, 480.00875328], - [ 166.54997871, 134.61857872, 108.80915072]], + [[ 773.49041181, 625.19479557, 505.33080493], + [ 592.52803177, 478.92699901, 387.10585505], + [ 134.31520045, 108.56393694, 87.74977339]], - [[ 1026.15144142, 696.85030591, 473.22483724], - [ 786.0776095 , 533.81830453, 362.51125692], - [ 178.18932781, 121.00678573, 82.17463061]]])) + [[ 545.9968869 , 370.78162375, 251.79449885], + [ 418.25788117, 284.03520249, 192.88577665], + [ 94.81136442, 64.38555328, 43.72365587]]])) expected['dni'] = (np. - array([[[ 1024.58284359, 942.20818604, 861.11344424], - [ 1024.58284359, 942.20818604, 861.11344424], - [ 1024.58284359, 942.20818604, 861.11344424]], + array([[[1014.38807396, 942.20818604, 861.11344424], + [1014.38807396, 942.20818604, 861.11344424], + [1014.38807396, 942.20818604, 861.11344424]], - [[ 687.61305142, 419.14891162, 255.50098235], - [ 687.61305142, 419.14891162, 255.50098235], - [ 687.61305142, 419.14891162, 255.50098235]], + [[ 687.61305142, 419.14891162, 255.50098235], + [ 687.61305142, 419.14891162, 255.50098235], + [ 687.61305142, 419.14891162, 255.50098235]], - [[ 458.62196014, 186.46177428, 75.80970012], - [ 458.62196014, 186.46177428, 75.80970012], - [ 458.62196014, 186.46177428, 75.80970012]]])) + [[ 458.62196014, 186.46177428, 75.80970012], + [ 458.62196014, 186.46177428, 75.80970012], + [ 458.62196014, 186.46177428, 75.80970012]]])) expected['dhi'] = (np. - array([[[ 82.20058349, 122.56094266, 163.23627919], - [ 62.96930021, 93.88712907, 125.04624459], - [ 14.27398153, 21.28248435, 28.34568241]], + array([[[ 81.38267402, 111.96631281, 153.04382915], + [ 62.3427452 , 85.77117175, 117.23837487], + [ 14.13195304, 19.44274618, 26.57578203]], - [[ 271.51004993, 356.08858598, 371.10594313], - [ 207.988765 , 272.77968255, 284.28364554], - [ 47.14722539, 61.83413404, 64.44187075]], + [[ 85.87736039, 206.04588395, 249.82982258], + [ 65.78587472, 157.84030442, 191.38074731], + [ 14.91244713, 35.77949226, 43.38249342]], - [[ 567.52948128, 510.38853163, 397.41513712], - [ 434.75280544, 390.98029849, 304.4376574 ], - [ 98.5504602 , 88.62803842, 69.01041434]]])) + [[ 87.37492676, 184.31984947, 175.98479873], + [ 66.93307711, 141.19719644, 134.81217714], + [ 15.17249681, 32.00680597, 30.5594396 ]]])) apparent_zenith = np.linspace(0, 80, 3) airmass_absolute = np.linspace(1, 10, 3) @@ -142,7 +174,7 @@ def test_ineichen_arrays(): def test_ineichen_dni_extra(): expected = pd.DataFrame( - np.array([[ 1053.20547182, 946.35279683, 121.22990042]]), + np.array([[1042.72590228, 946.35279683, 110.75033088]]), columns=['ghi', 'dni', 'dhi']) out = clearsky.ineichen(10, 1, 3, dni_extra=pd.Series(1370)) @@ -151,7 +183,7 @@ def test_ineichen_dni_extra(): def test_ineichen_altitude(): expected = pd.DataFrame( - np.array([[ 1145.64245696, 994.95377835, 165.80426215]]), + np.array([[1134.24312405, 994.95377835, 154.40492924]]), columns=['ghi', 'dni', 'dhi']) out = clearsky.ineichen(10, 1, 3, altitude=pd.Series(2000)) @@ -526,18 +558,18 @@ def test_detect_clearsky_components(detect_clearsky_data): assert_series_equal(expected['Clear or not'], clear_samples, check_dtype=False, check_names=False) assert isinstance(components, OrderedDict) - assert np.allclose(alpha, 0.95345573579557108) + assert np.allclose(alpha, 0.9633903181941296) @requires_scipy def test_detect_clearsky_iterations(detect_clearsky_data): expected, cs = detect_clearsky_data - alpha = 1.0348 + alpha = 1.0448 with pytest.warns(RuntimeWarning): clear_samples = clearsky.detect_clearsky( expected['GHI'], cs['ghi']*alpha, cs.index, 10, max_iterations=1) - assert (clear_samples[:'2012-04-01 10:39:00'] == True).all() - assert (clear_samples['2012-04-01 10:40:00':] == False).all() + assert (clear_samples[:'2012-04-01 10:41:00'] == True).all() + assert (clear_samples['2012-04-01 10:42:00':] == False).all() clear_samples = clearsky.detect_clearsky( expected['GHI'], cs['ghi']*alpha, cs.index, 10, max_iterations=20) assert_series_equal(expected['Clear or not'], clear_samples, diff --git a/pvlib/test/test_irradiance.py b/pvlib/test/test_irradiance.py index 31ebbcb532..489001fddc 100644 --- a/pvlib/test/test_irradiance.py +++ b/pvlib/test/test_irradiance.py @@ -9,29 +9,58 @@ from pandas.util.testing import assert_frame_equal, assert_series_equal -from pvlib.location import Location -from pvlib import clearsky -from pvlib import solarposition from pvlib import irradiance -from pvlib import atmosphere -from conftest import (requires_ephem, requires_numba, needs_numpy_1_10, - pandas_0_22) - -# setup times and location to be tested. -tus = Location(32.2, -111, 'US/Arizona', 700) - -# must include night values -times = pd.date_range(start='20140624', freq='6H', periods=4, tz=tus.tz) +from conftest import (needs_numpy_1_10, pandas_0_22, requires_ephem, + requires_numba) + + +# fixtures create realistic test input data +# test input data generated at Location(32.2, -111, 'US/Arizona', 700) +# test input data is hard coded to avoid dependencies on other parts of pvlib + + +@pytest.fixture +def times(): + # must include night values + return pd.date_range(start='20140624', freq='6H', periods=4, + tz='US/Arizona') + + +@pytest.fixture +def irrad_data(times): + return pd.DataFrame(np.array( + [[ 0. , 0. , 0. ], + [ 79.73860422, 316.1949056 , 40.46149818], + [1042.48031487, 939.95469881, 118.45831879], + [ 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 , + 352.69550699, -2.36677158], + [ 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, + 287.04104128, -2.52831909]]), + columns=['apparent_zenith', 'zenith', 'apparent_elevation', + 'elevation', 'azimuth', 'equation_of_time'], + index=times) -ephem_data = solarposition.get_solarposition( - times, tus.latitude, tus.longitude, method='nrel_numpy') -irrad_data = tus.get_clearsky(times, model='ineichen', linke_turbidity=3) +@pytest.fixture +def dni_et(times): + return irradiance.extraradiation(times.dayofyear) -dni_et = irradiance.extraradiation(times.dayofyear) -ghi = irrad_data['ghi'] +@pytest.fixture +def relative_airmass(times): + return pd.Series([np.nan, 7.58831596, 1.01688136, 3.27930443], times) # setup for et rad test. put it here for readability @@ -63,9 +92,11 @@ def test_extraradiation(input, expected, method): @requires_numba -def test_extraradiation_nrel_numba(): - result = irradiance.extraradiation(times, method='nrel', how='numba', numthreads=8) - assert_allclose(result, [1322.332316, 1322.296282, 1322.261205, 1322.227091]) +def test_extraradiation_nrel_numba(times): + result = irradiance.extraradiation(times, method='nrel', how='numba', + numthreads=4) + assert_allclose(result, + [1322.332316, 1322.296282, 1322.261205, 1322.227091]) def test_extraradiation_epoch_year(): @@ -83,23 +114,24 @@ def test_grounddiffuse_simple_float(): assert_allclose(result, 26.32000014911496) -def test_grounddiffuse_simple_series(): - ground_irrad = irradiance.grounddiffuse(40, ghi) +def test_grounddiffuse_simple_series(irrad_data): + ground_irrad = irradiance.grounddiffuse(40, irrad_data['ghi']) assert ground_irrad.name == 'diffuse_ground' -def test_grounddiffuse_albedo_0(): - ground_irrad = irradiance.grounddiffuse(40, ghi, albedo=0) +def test_grounddiffuse_albedo_0(irrad_data): + ground_irrad = irradiance.grounddiffuse(40, irrad_data['ghi'], albedo=0) assert 0 == ground_irrad.all() -def test_grounddiffuse_albedo_invalid_surface(): +def test_grounddiffuse_albedo_invalid_surface(irrad_data): with pytest.raises(KeyError): - irradiance.grounddiffuse(40, ghi, surface_type='invalid') + irradiance.grounddiffuse(40, irrad_data['ghi'], surface_type='invalid') -def test_grounddiffuse_albedo_surface(): - result = irradiance.grounddiffuse(40, ghi, surface_type='sand') +def test_grounddiffuse_albedo_surface(irrad_data): + result = irradiance.grounddiffuse(40, irrad_data['ghi'], + surface_type='sand') assert_allclose(result, [0, 3.731058, 48.778813, 12.035025], atol=1e-4) @@ -108,7 +140,7 @@ def test_isotropic_float(): assert_allclose(result, 88.30222215594891) -def test_isotropic_series(): +def test_isotropic_series(irrad_data): result = irradiance.isotropic(40, irrad_data['dhi']) assert_allclose(result, [0, 35.728402, 104.601328, 54.777191], atol=1e-4) @@ -130,7 +162,7 @@ def test_klucher_series_float(): assert_allclose(result, expected[0]) -def test_klucher_series(): +def test_klucher_series(irrad_data, ephem_data): result = irradiance.klucher(40, 180, irrad_data['dhi'], irrad_data['ghi'], ephem_data['apparent_zenith'], ephem_data['azimuth']) @@ -143,7 +175,7 @@ def test_klucher_series(): assert_allclose(result, expected, atol=1e-4) -def test_haydavies(): +def test_haydavies(irrad_data, ephem_data, dni_et): result = irradiance.haydavies(40, 180, irrad_data['dhi'], irrad_data['dni'], dni_et, ephem_data['apparent_zenith'], @@ -151,7 +183,7 @@ def test_haydavies(): assert_allclose(result, [0, 14.967008, 102.994862, 33.190865], atol=1e-4) -def test_reindl(): +def test_reindl(irrad_data, ephem_data, dni_et): result = irradiance.reindl(40, 180, irrad_data['dhi'], irrad_data['dni'], irrad_data['ghi'], dni_et, ephem_data['apparent_zenith'], @@ -159,41 +191,40 @@ def test_reindl(): assert_allclose(result, [np.nan, 15.730664, 104.131724, 34.166258], atol=1e-4) -def test_king(): +def test_king(irrad_data, ephem_data): result = irradiance.king(40, irrad_data['dhi'], irrad_data['ghi'], ephem_data['apparent_zenith']) assert_allclose(result, [0, 44.629352, 115.182626, 79.719855], atol=1e-4) -def test_perez(): - am = atmosphere.relativeairmass(ephem_data['apparent_zenith']) +def test_perez(irrad_data, ephem_data, dni_et, relative_airmass): dni = irrad_data['dni'].copy() dni.iloc[2] = np.nan out = irradiance.perez(40, 180, irrad_data['dhi'], dni, dni_et, ephem_data['apparent_zenith'], - ephem_data['azimuth'], am) + ephem_data['azimuth'], relative_airmass) expected = pd.Series(np.array( [ 0. , 31.46046871, np.nan, 45.45539877]), - index=times) + index=irrad_data.index) assert_series_equal(out, expected, check_less_precise=2) -def test_perez_components(): - am = atmosphere.relativeairmass(ephem_data['apparent_zenith']) +def test_perez_components(irrad_data, ephem_data, dni_et, relative_airmass): dni = irrad_data['dni'].copy() dni.iloc[2] = np.nan out, df_components = irradiance.perez(40, 180, irrad_data['dhi'], dni, dni_et, ephem_data['apparent_zenith'], - ephem_data['azimuth'], am, return_components=True) + ephem_data['azimuth'], relative_airmass, + return_components=True) expected = pd.Series(np.array( [ 0. , 31.46046871, np.nan, 45.45539877]), - index=times) + index=irrad_data.index) expected_components = pd.DataFrame( np.array([[ 0. , 26.84138589, np.nan, 31.72696071], [ 0. , 0. , np.nan, 4.47966439], [ 0. , 4.62212181, np.nan, 9.25316454]]).T, columns=['isotropic', 'circumsolar', 'horizon'], - index=times + index=irrad_data.index ) if pandas_0_22(): expected_for_sum = expected.copy() @@ -206,14 +237,14 @@ def test_perez_components(): assert_frame_equal(df_components, expected_components) assert_series_equal(sum_components, expected_for_sum, check_less_precise=2) + @needs_numpy_1_10 -def test_perez_arrays(): - am = atmosphere.relativeairmass(ephem_data['apparent_zenith']) +def test_perez_arrays(irrad_data, ephem_data, dni_et, relative_airmass): dni = irrad_data['dni'].copy() dni.iloc[2] = np.nan out = irradiance.perez(40, 180, irrad_data['dhi'].values, dni.values, dni_et, ephem_data['apparent_zenith'].values, - ephem_data['azimuth'].values, am.values) + ephem_data['azimuth'].values, relative_airmass.values) expected = np.array( [ 0. , 31.46046871, np.nan, 45.45539877]) assert_allclose(out, expected, atol=1e-2) @@ -230,10 +261,9 @@ def test_liujordan(): # klutcher (misspelling) will be removed in 0.3 -def test_total_irrad(): +def test_total_irrad(irrad_data, ephem_data, dni_et, relative_airmass): models = ['isotropic', 'klutcher', 'klucher', 'haydavies', 'reindl', 'king', 'perez'] - AM = atmosphere.relativeairmass(ephem_data['apparent_zenith']) for model in models: total = irradiance.total_irrad( @@ -241,7 +271,7 @@ def test_total_irrad(): ephem_data['apparent_zenith'], ephem_data['azimuth'], dni=irrad_data['dni'], ghi=irrad_data['ghi'], dhi=irrad_data['dhi'], - dni_extra=dni_et, airmass=AM, + dni_extra=dni_et, airmass=relative_airmass, model=model, surface_type='urban') @@ -269,23 +299,21 @@ def test_total_irrad_scalars(model): assert np.isnan(np.array(list(total.values()))).sum() == 0 -def test_globalinplane(): +def test_globalinplane(irrad_data, ephem_data, dni_et, relative_airmass): aoi = irradiance.aoi(40, 180, ephem_data['apparent_zenith'], ephem_data['azimuth']) - airmass = atmosphere.relativeairmass(ephem_data['apparent_zenith']) - gr_sand = irradiance.grounddiffuse(40, ghi, surface_type='sand') + gr_sand = irradiance.grounddiffuse(40, irrad_data['ghi'], + surface_type='sand') diff_perez = irradiance.perez( 40, 180, irrad_data['dhi'], irrad_data['dni'], dni_et, - ephem_data['apparent_zenith'], ephem_data['azimuth'], airmass) + ephem_data['apparent_zenith'], ephem_data['azimuth'], relative_airmass) irradiance.globalinplane( aoi=aoi, dni=irrad_data['dni'], poa_sky_diffuse=diff_perez, poa_ground_diffuse=gr_sand) -def test_disc_keys(): - clearsky_data = tus.get_clearsky(times, model='ineichen', - linke_turbidity=3) - disc_data = irradiance.disc(clearsky_data['ghi'], ephem_data['zenith'], +def test_disc_keys(irrad_data, ephem_data): + disc_data = irradiance.disc(irrad_data['ghi'], ephem_data['zenith'], ephem_data.index) assert 'dni' in disc_data.columns assert 'kt' in disc_data.columns @@ -302,14 +330,6 @@ def test_disc_value(): np.array([830.46, 676.09]), 1) -def test_dirint(): - clearsky_data = tus.get_clearsky(times, model='ineichen', - linke_turbidity=3) - pressure = 93193. - dirint_data = irradiance.dirint(clearsky_data['ghi'], ephem_data['zenith'], - ephem_data.index, pressure=pressure) - - def test_dirint_value(): times = pd.DatetimeIndex(['2014-06-24T12-0700','2014-06-24T18-0700']) ghi = pd.Series([1038.62, 254.53], index=times) @@ -394,9 +414,7 @@ def test_erbs_all_scalar(): @needs_numpy_1_10 -def test_dirindex(): - clearsky_data = tus.get_clearsky(times, model='ineichen', - linke_turbidity=3) +def test_dirindex(times): ghi = pd.Series([0, 0, 1038.62, 254.53], index=times) ghi_clearsky = pd.Series( np.array([0., 79.73860422, 1042.48031487, 257.20751138]), diff --git a/pvlib/test/test_location.py b/pvlib/test/test_location.py index bc18583b83..fd4a48d359 100644 --- a/pvlib/test/test_location.py +++ b/pvlib/test/test_location.py @@ -1,20 +1,27 @@ import datetime +try: + from unittest.mock import ANY +except ImportError: + # python 2 + from mock import ANY import numpy as np from numpy import nan import pandas as pd -import pytz +from pandas.util.testing import assert_frame_equal, assert_index_equal import pytest + +import pytz from pytz.exceptions import UnknownTimeZoneError -from pandas.util.testing import assert_series_equal, assert_frame_equal +import pvlib from pvlib.location import Location from test_solarposition import expected_solpos + from conftest import requires_scipy -aztz = pytz.timezone('US/Arizona') def test_location_required(): Location(32.2, -111) @@ -24,7 +31,7 @@ def test_location_all(): @pytest.mark.parametrize('tz', [ - aztz, 'America/Phoenix', -7, -7.0, + pytz.timezone('US/Arizona'), 'America/Phoenix', -7, -7.0, ]) def test_location_tz(tz): Location(32.2, -111, tz) @@ -49,11 +56,12 @@ def test_location_print_all(): ' longitude: -111', ' altitude: 700', ' tz: US/Arizona' -]) + ]) assert tus.__str__() == expected_str + def test_location_print_pytz(): - tus = Location(32.2, -111, aztz, 700, 'Tucson') + tus = Location(32.2, -111, pytz.timezone('US/Arizona'), 700, 'Tucson') expected_str = '\n'.join([ 'Location: ', ' name: Tucson', @@ -66,40 +74,38 @@ def test_location_print_pytz(): @requires_scipy -def test_get_clearsky(): +def test_get_clearsky(mocker): tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') times = pd.DatetimeIndex(start='20160101T0600-0700', end='20160101T1800-0700', freq='3H') - clearsky = tus.get_clearsky(times) - expected = pd.DataFrame(data=np.array([ - ( 0.0, 0.0, 0.0), - (262.77734276159333, 791.1972825869296, 46.18714900637892), - (616.764693938387, 974.9610353623959, 65.44157429054201), - (419.6512657626518, 901.6234995035793, 54.26016437839348), - ( 0.0, 0.0, 0.0)], - dtype=[('ghi', ' 0 during the day + assert (out.iloc[1:-1, :] > 0).all().all() + assert (out.columns.values == ['ghi', 'dni', 'dhi']).all() + + +def test_get_clearsky_ineichen_supply_linke(mocker): tus = Location(32.2, -111, 'US/Arizona', 700) - times = pd.date_range(start='2014-06-24', end='2014-06-25', freq='3h') - times_localized = times.tz_localize(tus.tz) - expected = pd.DataFrame(np. - array([[ 0. , 0. , 0. ], - [ 0. , 0. , 0. ], - [ 79.73090244, 316.16436502, 40.45759009], - [ 703.43653498, 876.41452667, 95.15798252], - [ 1042.37962396, 939.86391062, 118.44687715], - [ 851.32411813, 909.11186737, 105.36662462], - [ 257.18266827, 646.16644264, 62.02777094], - [ 0. , 0. , 0. ], - [ 0. , 0. , 0. ]]), - columns=['ghi', 'dni', 'dhi'], - index=times_localized) - out = tus.get_clearsky(times_localized, linke_turbidity=3) - assert_frame_equal(expected, out, check_less_precise=2) + times = pd.date_range(start='2014-06-24-0700', end='2014-06-25-0700', + freq='3h') + mocker.spy(pvlib.clearsky, 'ineichen') + out = tus.get_clearsky(times, linke_turbidity=3) + # we only care that the LT is passed in this test + pvlib.clearsky.ineichen.assert_called_once_with(ANY, ANY, 3, ANY, ANY) + assert_index_equal(out.index, times) + # check that values are 0 before sunrise and after sunset + assert out.iloc[0:2, :].sum().sum() == 0 + assert out.iloc[-2:, :].sum().sum() == 0 + # check that values are > 0 during the day + assert (out.iloc[2:-2, :] > 0).all().all() + assert (out.columns.values == ['ghi', 'dni', 'dhi']).all() def test_get_clearsky_haurwitz(): diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index 9c908e366f..0dab068713 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -1,4 +1,9 @@ import sys +try: + from unittest.mock import ANY +except ImportError: + # python 2 + from mock import ANY import numpy as np import pandas as pd @@ -119,7 +124,7 @@ def test_run_model(system, location): expected = pd.Series(np.array([ 183.522449305, -2.00000000e-02]), index=times) - assert_series_equal(ac, expected, check_less_precise=2) + assert_series_equal(ac, expected, check_less_precise=1) def test_run_model_with_irradiance(system, location): @@ -159,132 +164,140 @@ def test_run_model_gueymard_perez(system, location): assert_series_equal(ac, expected) -@requires_scipy -def test_run_model_with_weather(system, location): +def test_run_model_with_weather(system, location, weather, mocker): mc = ModelChain(system, location) - times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') - weather = pd.DataFrame({'wind_speed':5, 'temp_air':10}, index=times) - ac = mc.run_model(times, weather=weather).ac - - expected = pd.Series(np.array([ 201.691634921, -2.00000000e-02]), - index=times) - assert_series_equal(ac, expected, check_less_precise=2) + m = mocker.spy(system, 'sapm_celltemp') + weather['wind_speed'] = 5 + weather['temp_air'] = 10 + mc.run_model(weather.index, weather=weather) + assert m.call_count == 1 + # assert_called_once_with cannot be used with series, so need to use + # assert_series_equal on call_args + assert_series_equal(m.call_args[0][1], weather['wind_speed']) # wind + assert_series_equal(m.call_args[0][2], weather['temp_air']) # temp + assert not mc.ac.empty -@requires_scipy -def test_run_model_tracker(system, location): +def test_run_model_tracker(system, location, weather, mocker): system = SingleAxisTracker(module_parameters=system.module_parameters, inverter_parameters=system.inverter_parameters) + mocker.spy(system, 'singleaxis') mc = ModelChain(system, location) - times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') - ac = mc.run_model(times).ac - - expected = pd.Series(np.array([119.067713606, nan]), - index=times) - assert_series_equal(ac, expected, check_less_precise=2) - - expect = pd.DataFrame(np. - array([[ 54.82513187, 90. , 11.0039221 , 11.0039221 ], - [ nan, 0. , 0. , nan]]), - columns=['aoi', 'surface_azimuth', 'surface_tilt', 'tracker_theta'], - index=times) - expect = expect[['tracker_theta', 'aoi', 'surface_azimuth', 'surface_tilt']] - assert_frame_equal(mc.tracking, expect, check_less_precise=2) + mc.run_model(weather.index, weather=weather) + assert system.singleaxis.call_count == 1 + assert (mc.tracking.columns == ['tracker_theta', 'aoi', 'surface_azimuth', + 'surface_tilt']).all() + assert mc.ac[0] > 0 + assert np.isnan(mc.ac[1]) def poadc(mc): mc.dc = mc.total_irrad['poa_global'] * 0.2 mc.dc.name = None # assert_series_equal will fail without this -@requires_scipy -@pytest.mark.parametrize('dc_model, expected', [ - ('sapm', [181.604438144, -2.00000000e-02]), - ('singlediode', [181.044109596, -2.00000000e-02]), - ('pvwatts', [190.028186986, 0]), - (poadc, [189.183065667, 0]) # user supplied function -]) -def test_dc_models(system, cec_dc_snl_ac_system, pvwatts_dc_pvwatts_ac_system, - location, dc_model, expected): - - dc_systems = {'sapm': system, 'singlediode': cec_dc_snl_ac_system, - 'pvwatts': pvwatts_dc_pvwatts_ac_system, - poadc: pvwatts_dc_pvwatts_ac_system} - system = dc_systems[dc_model] - - mc = ModelChain(system, location, dc_model=dc_model, - aoi_model='no_loss', spectral_model='no_loss') - times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') - ac = mc.run_model(times).ac - - expected = pd.Series(np.array(expected), index=times) - assert_series_equal(ac, expected, check_less_precise=2) - - -@requires_scipy -@pytest.mark.parametrize('dc_model', ['sapm', 'singlediode', 'pvwatts_dc']) +@pytest.mark.parametrize('dc_model', [ + 'sapm', pytest.param('singlediode', marks=requires_scipy), 'pvwatts_dc']) def test_infer_dc_model(system, cec_dc_snl_ac_system, pvwatts_dc_pvwatts_ac_system, location, dc_model, - mocker): + weather, mocker): dc_systems = {'sapm': system, 'singlediode': cec_dc_snl_ac_system, 'pvwatts_dc': pvwatts_dc_pvwatts_ac_system} system = dc_systems[dc_model] m = mocker.spy(system, dc_model) mc = ModelChain(system, location, aoi_model='no_loss', spectral_model='no_loss') - times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') - mc.run_model(times) + mc.run_model(weather.index, weather=weather) assert m.call_count == 1 assert isinstance(mc.dc, (pd.Series, pd.DataFrame)) +def test_dc_model_user_func(pvwatts_dc_pvwatts_ac_system, location, weather, + mocker): + m = mocker.spy(sys.modules[__name__], 'poadc') + mc = ModelChain(pvwatts_dc_pvwatts_ac_system, location, dc_model=poadc, + aoi_model='no_loss', spectral_model='no_loss') + mc.run_model(weather.index, weather=weather) + assert m.call_count == 1 + assert isinstance(mc.ac, (pd.Series, pd.DataFrame)) + assert not mc.ac.empty + + def acdc(mc): mc.ac = mc.dc -@requires_scipy -@pytest.mark.parametrize('ac_model, expected', [ - ('snlinverter', [181.604438144, -2.00000000e-02]), - ('adrinverter', [np.nan, -25.00000000e-02]), - ('pvwatts', [190.028186986, 0]), - (acdc, [199.845296258, 0]) # user supplied function -]) -def test_ac_models(system, cec_dc_adr_ac_system, pvwatts_dc_pvwatts_ac_system, - location, ac_model, expected): +@pytest.mark.parametrize('ac_model', [ + 'snlinverter', pytest.param('adrinverter', marks=requires_scipy), + 'pvwatts']) +def test_ac_models(system, cec_dc_adr_ac_system, pvwatts_dc_pvwatts_ac_system, + location, ac_model, weather, mocker): ac_systems = {'snlinverter': system, 'adrinverter': cec_dc_adr_ac_system, - 'pvwatts': pvwatts_dc_pvwatts_ac_system, - acdc: pvwatts_dc_pvwatts_ac_system} - + 'pvwatts': pvwatts_dc_pvwatts_ac_system} system = ac_systems[ac_model] mc = ModelChain(system, location, ac_model=ac_model, aoi_model='no_loss', spectral_model='no_loss') - times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') - ac = mc.run_model(times).ac + if ac_model == 'pvwatts': + ac_model += '_ac' + m = mocker.spy(system, ac_model) + mc.run_model(weather.index, weather=weather) + assert m.call_count == 1 + assert isinstance(mc.ac, pd.Series) + assert not mc.ac.empty + assert mc.ac[1] < 1 - expected = pd.Series(np.array(expected), index=times) - assert_series_equal(ac, expected, check_less_precise=2) + +def test_ac_model_user_func(pvwatts_dc_pvwatts_ac_system, location, weather, + mocker): + m = mocker.spy(sys.modules[__name__], 'acdc') + mc = ModelChain(pvwatts_dc_pvwatts_ac_system, location, ac_model=acdc, + aoi_model='no_loss', spectral_model='no_loss') + mc.run_model(weather.index, weather=weather) + assert m.call_count == 1 + assert_series_equal(mc.ac, mc.dc) + assert not mc.ac.empty def constant_aoi_loss(mc): mc.aoi_modifier = 0.9 -@requires_scipy -@pytest.mark.parametrize('aoi_model, expected', [ - ('sapm', [182.784057666, -2.00000000e-02]), - ('ashrae', [180.825930547, -2.00000000e-02]), - ('physical', [181.453077805, -2.00000000e-02]), - ('no_loss', [181.604438144, -2.00000000e-02]), - (constant_aoi_loss, [164.997043305, -2e-2]) -]) -def test_aoi_models(system, location, aoi_model, expected): + +@pytest.mark.parametrize('aoi_model, method', [ + ('sapm', 'sapm_aoi_loss'), ('ashrae', 'ashraeiam'), + ('physical', 'physicaliam')]) +def test_aoi_models(system, location, aoi_model, method, weather, mocker): mc = ModelChain(system, location, dc_model='sapm', aoi_model=aoi_model, spectral_model='no_loss') - times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') - ac = mc.run_model(times).ac + m = mocker.spy(system, method) + mc.run_model(weather.index, weather=weather) + assert m.call_count == 1 + assert isinstance(mc.ac, pd.Series) + assert not mc.ac.empty + assert mc.ac[0] > 150 and mc.ac[0] < 200 + assert mc.ac[1] < 1 + - expected = pd.Series(np.array(expected), index=times) - assert_series_equal(ac, expected, check_less_precise=2) +def test_aoi_model_no_loss(system, location, weather): + mc = ModelChain(system, location, dc_model='sapm', + aoi_model='no_loss', spectral_model='no_loss') + mc.run_model(weather.index, weather=weather) + assert mc.aoi_modifier == 1.0 + assert not mc.ac.empty + assert mc.ac[0] > 150 and mc.ac[0] < 200 + assert mc.ac[1] < 1 + + +def test_aoi_model_user_func(system, location, weather, mocker): + m = mocker.spy(sys.modules[__name__], 'constant_aoi_loss') + mc = ModelChain(system, location, dc_model='sapm', + aoi_model=constant_aoi_loss, spectral_model='no_loss') + mc.run_model(weather.index, weather=weather) + assert m.call_count == 1 + assert mc.aoi_modifier == 0.9 + assert not mc.ac.empty + assert mc.ac[0] > 140 and mc.ac[0] < 200 + assert mc.ac[1] < 1 def constant_spectral_loss(mc): @@ -408,7 +421,7 @@ def test_basic_chain_alt_az(sam_data): expected = pd.Series(np.array([ 115.40352679, -2.00000000e-02]), index=times) - assert_series_equal(ac, expected, check_less_precise=2) + assert_series_equal(ac, expected, check_less_precise=1) @requires_scipy @@ -430,7 +443,7 @@ def test_basic_chain_strategy(sam_data): expected = pd.Series(np.array([ 183.522449305, -2.00000000e-02]), index=times) - assert_series_equal(ac, expected, check_less_precise=2) + assert_series_equal(ac, expected, check_less_precise=1) @requires_scipy @@ -455,7 +468,7 @@ def test_basic_chain_altitude_pressure(sam_data): expected = pd.Series(np.array([ 116.595664887, -2.00000000e-02]), index=times) - assert_series_equal(ac, expected, check_less_precise=2) + assert_series_equal(ac, expected, check_less_precise=1) dc, ac = modelchain.basic_chain(times, latitude, longitude, module_parameters, inverter_parameters, @@ -465,7 +478,7 @@ def test_basic_chain_altitude_pressure(sam_data): expected = pd.Series(np.array([ 116.595664887, -2.00000000e-02]), index=times) - assert_series_equal(ac, expected, check_less_precise=2) + assert_series_equal(ac, expected, check_less_precise=1) @pytest.mark.parametrize('strategy, strategy_str', [