pvlib 0.11.0__py3-none-any.whl → 0.11.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pvlib/atmosphere.py +157 -1
- pvlib/bifacial/__init__.py +4 -4
- pvlib/bifacial/loss_models.py +163 -0
- pvlib/clearsky.py +18 -29
- pvlib/data/pvgis_tmy_meta.json +32 -93
- pvlib/data/pvgis_tmy_test.dat +8761 -8761
- pvlib/data/tmy_45.000_8.000_2005_2020.csv +8789 -0
- pvlib/data/tmy_45.000_8.000_2005_2020.epw +8768 -0
- pvlib/data/tmy_45.000_8.000_2005_2020.json +1 -0
- pvlib/data/tmy_45.000_8.000_2005_2020.txt +8761 -0
- pvlib/data/tmy_45.000_8.000_userhorizon.json +1 -1
- pvlib/data/variables_style_rules.csv +2 -1
- pvlib/iotools/pvgis.py +39 -3
- pvlib/irradiance.py +141 -120
- pvlib/location.py +5 -5
- pvlib/modelchain.py +1 -1
- pvlib/pvsystem.py +2 -2
- pvlib/shading.py +8 -8
- pvlib/singlediode.py +1 -1
- pvlib/solarposition.py +55 -50
- pvlib/spa.py +24 -22
- pvlib/spectrum/__init__.py +9 -4
- pvlib/spectrum/irradiance.py +272 -0
- pvlib/spectrum/mismatch.py +118 -508
- pvlib/spectrum/response.py +280 -0
- pvlib/spectrum/spectrl2.py +16 -16
- pvlib/tests/bifacial/test_losses_models.py +54 -0
- pvlib/tests/iotools/test_pvgis.py +57 -11
- pvlib/tests/spectrum/__init__.py +0 -0
- pvlib/tests/spectrum/conftest.py +40 -0
- pvlib/tests/spectrum/test_irradiance.py +138 -0
- pvlib/tests/{test_spectrum.py → spectrum/test_mismatch.py} +32 -306
- pvlib/tests/spectrum/test_response.py +124 -0
- pvlib/tests/spectrum/test_spectrl2.py +72 -0
- pvlib/tests/test_atmosphere.py +71 -0
- pvlib/tests/test_clearsky.py +37 -25
- pvlib/tests/test_irradiance.py +6 -6
- pvlib/tests/test_solarposition.py +84 -36
- pvlib/tests/test_spa.py +1 -1
- pvlib/tools.py +26 -2
- pvlib/tracking.py +53 -47
- {pvlib-0.11.0.dist-info → pvlib-0.11.1.dist-info}/METADATA +31 -29
- {pvlib-0.11.0.dist-info → pvlib-0.11.1.dist-info}/RECORD +47 -38
- {pvlib-0.11.0.dist-info → pvlib-0.11.1.dist-info}/WHEEL +1 -1
- pvlib/data/tmy_45.000_8.000_2005_2016.csv +0 -8789
- pvlib/data/tmy_45.000_8.000_2005_2016.epw +0 -8768
- pvlib/data/tmy_45.000_8.000_2005_2016.json +0 -1
- pvlib/data/tmy_45.000_8.000_2005_2016.txt +0 -8761
- {pvlib-0.11.0.dist-info → pvlib-0.11.1.dist-info}/AUTHORS.md +0 -0
- {pvlib-0.11.0.dist-info → pvlib-0.11.1.dist-info}/LICENSE +0 -0
- {pvlib-0.11.0.dist-info → pvlib-0.11.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
from pvlib import spectrum
|
|
5
|
+
from numpy.testing import assert_allclose
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_spectrl2(spectrl2_data):
|
|
9
|
+
# compare against output from solar_utils wrapper around NREL spectrl2_2.c
|
|
10
|
+
kwargs, expected = spectrl2_data
|
|
11
|
+
actual = spectrum.spectrl2(**kwargs)
|
|
12
|
+
assert_allclose(expected['wavelength'].values, actual['wavelength'])
|
|
13
|
+
assert_allclose(expected['specdif'].values, actual['dhi'].ravel(),
|
|
14
|
+
atol=7e-5)
|
|
15
|
+
assert_allclose(expected['specdir'].values, actual['dni'].ravel(),
|
|
16
|
+
atol=1.5e-4)
|
|
17
|
+
assert_allclose(expected['specetr'], actual['dni_extra'].ravel(),
|
|
18
|
+
atol=2e-4)
|
|
19
|
+
assert_allclose(expected['specglo'], actual['poa_global'].ravel(),
|
|
20
|
+
atol=1e-4)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_spectrl2_array(spectrl2_data):
|
|
24
|
+
# test that supplying arrays instead of scalars works
|
|
25
|
+
kwargs, expected = spectrl2_data
|
|
26
|
+
kwargs = {k: np.array([v, v, v]) for k, v in kwargs.items()}
|
|
27
|
+
actual = spectrum.spectrl2(**kwargs)
|
|
28
|
+
|
|
29
|
+
assert actual['wavelength'].shape == (122,)
|
|
30
|
+
|
|
31
|
+
keys = ['dni_extra', 'dhi', 'dni', 'poa_sky_diffuse', 'poa_ground_diffuse',
|
|
32
|
+
'poa_direct', 'poa_global']
|
|
33
|
+
for key in keys:
|
|
34
|
+
assert actual[key].shape == (122, 3)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_spectrl2_series(spectrl2_data):
|
|
38
|
+
# test that supplying Series instead of scalars works
|
|
39
|
+
kwargs, expected = spectrl2_data
|
|
40
|
+
kwargs.pop('dayofyear')
|
|
41
|
+
index = pd.to_datetime(['2020-03-15 10:45:59']*3)
|
|
42
|
+
kwargs = {k: pd.Series([v, v, v], index=index) for k, v in kwargs.items()}
|
|
43
|
+
actual = spectrum.spectrl2(**kwargs)
|
|
44
|
+
|
|
45
|
+
assert actual['wavelength'].shape == (122,)
|
|
46
|
+
|
|
47
|
+
keys = ['dni_extra', 'dhi', 'dni', 'poa_sky_diffuse', 'poa_ground_diffuse',
|
|
48
|
+
'poa_direct', 'poa_global']
|
|
49
|
+
for key in keys:
|
|
50
|
+
assert actual[key].shape == (122, 3)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_dayofyear_missing(spectrl2_data):
|
|
54
|
+
# test that not specifying dayofyear with non-pandas inputs raises error
|
|
55
|
+
kwargs, expected = spectrl2_data
|
|
56
|
+
kwargs.pop('dayofyear')
|
|
57
|
+
with pytest.raises(ValueError, match='dayofyear must be specified'):
|
|
58
|
+
_ = spectrum.spectrl2(**kwargs)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_aoi_gt_90(spectrl2_data):
|
|
62
|
+
# test that returned irradiance values are non-negative when aoi > 90
|
|
63
|
+
# see GH #1348
|
|
64
|
+
kwargs, _ = spectrl2_data
|
|
65
|
+
kwargs['apparent_zenith'] = 70
|
|
66
|
+
kwargs['aoi'] = 130
|
|
67
|
+
kwargs['surface_tilt'] = 60
|
|
68
|
+
|
|
69
|
+
spectra = spectrum.spectrl2(**kwargs)
|
|
70
|
+
for key in ['poa_direct', 'poa_global']:
|
|
71
|
+
message = f'{key} contains negative values for aoi>90'
|
|
72
|
+
assert np.all(spectra[key] >= 0), message
|
pvlib/tests/test_atmosphere.py
CHANGED
|
@@ -131,3 +131,74 @@ def test_bird_hulstrom80_aod_bb():
|
|
|
131
131
|
aod380, aod500 = 0.22072480948195175, 0.1614279181106312
|
|
132
132
|
bird_hulstrom = atmosphere.bird_hulstrom80_aod_bb(aod380, aod500)
|
|
133
133
|
assert np.isclose(0.11738229553812768, bird_hulstrom)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@pytest.fixture
|
|
137
|
+
def windspeeds_data_powerlaw():
|
|
138
|
+
data = pd.DataFrame(
|
|
139
|
+
index=pd.date_range(start="2015-01-01 00:00", end="2015-01-01 05:00",
|
|
140
|
+
freq="1h"),
|
|
141
|
+
columns=["wind_ref", "height_ref", "height_desired", "wind_calc"],
|
|
142
|
+
data=[
|
|
143
|
+
(10, -2, 5, np.nan),
|
|
144
|
+
(-10, 2, 5, np.nan),
|
|
145
|
+
(5, 4, 5, 5.067393209486324),
|
|
146
|
+
(7, 6, 10, 7.2178684911195905),
|
|
147
|
+
(10, 8, 20, 10.565167835216586),
|
|
148
|
+
(12, 10, 30, 12.817653329393977)
|
|
149
|
+
]
|
|
150
|
+
)
|
|
151
|
+
return data
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_windspeed_powerlaw_ndarray(windspeeds_data_powerlaw):
|
|
155
|
+
# test wind speed estimation by passing in surface_type
|
|
156
|
+
result_surface = atmosphere.windspeed_powerlaw(
|
|
157
|
+
windspeeds_data_powerlaw["wind_ref"].to_numpy(),
|
|
158
|
+
windspeeds_data_powerlaw["height_ref"],
|
|
159
|
+
windspeeds_data_powerlaw["height_desired"],
|
|
160
|
+
surface_type='unstable_air_above_open_water_surface')
|
|
161
|
+
assert_allclose(windspeeds_data_powerlaw["wind_calc"].to_numpy(),
|
|
162
|
+
result_surface)
|
|
163
|
+
# test wind speed estimation by passing in the exponent corresponding
|
|
164
|
+
# to the surface_type above
|
|
165
|
+
result_exponent = atmosphere.windspeed_powerlaw(
|
|
166
|
+
windspeeds_data_powerlaw["wind_ref"].to_numpy(),
|
|
167
|
+
windspeeds_data_powerlaw["height_ref"],
|
|
168
|
+
windspeeds_data_powerlaw["height_desired"],
|
|
169
|
+
exponent=0.06)
|
|
170
|
+
assert_allclose(windspeeds_data_powerlaw["wind_calc"].to_numpy(),
|
|
171
|
+
result_exponent)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_windspeed_powerlaw_series(windspeeds_data_powerlaw):
|
|
175
|
+
result = atmosphere.windspeed_powerlaw(
|
|
176
|
+
windspeeds_data_powerlaw["wind_ref"],
|
|
177
|
+
windspeeds_data_powerlaw["height_ref"],
|
|
178
|
+
windspeeds_data_powerlaw["height_desired"],
|
|
179
|
+
surface_type='unstable_air_above_open_water_surface')
|
|
180
|
+
assert_series_equal(windspeeds_data_powerlaw["wind_calc"],
|
|
181
|
+
result, check_names=False)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_windspeed_powerlaw_invalid():
|
|
185
|
+
with pytest.raises(ValueError, match="Either a 'surface_type' or an "
|
|
186
|
+
"'exponent' parameter must be given"):
|
|
187
|
+
# no exponent or surface_type given
|
|
188
|
+
atmosphere.windspeed_powerlaw(wind_speed_reference=10,
|
|
189
|
+
height_reference=5,
|
|
190
|
+
height_desired=10)
|
|
191
|
+
with pytest.raises(ValueError, match="Either a 'surface_type' or an "
|
|
192
|
+
"'exponent' parameter must be given"):
|
|
193
|
+
# no exponent or surface_type given
|
|
194
|
+
atmosphere.windspeed_powerlaw(wind_speed_reference=10,
|
|
195
|
+
height_reference=5,
|
|
196
|
+
height_desired=10,
|
|
197
|
+
exponent=1.2,
|
|
198
|
+
surface_type="surf")
|
|
199
|
+
with pytest.raises(KeyError, match='not_an_exponent'):
|
|
200
|
+
# invalid surface_type
|
|
201
|
+
atmosphere.windspeed_powerlaw(wind_speed_reference=10,
|
|
202
|
+
height_reference=5,
|
|
203
|
+
height_desired=10,
|
|
204
|
+
surface_type='not_an_exponent')
|
pvlib/tests/test_clearsky.py
CHANGED
|
@@ -675,13 +675,18 @@ def test_detect_clearsky_missing_index(detect_clearsky_data):
|
|
|
675
675
|
def test_detect_clearsky_not_enough_data(detect_clearsky_data):
|
|
676
676
|
expected, cs = detect_clearsky_data
|
|
677
677
|
with pytest.raises(ValueError, match='have at least'):
|
|
678
|
-
|
|
678
|
+
clearsky.detect_clearsky(expected['GHI'], cs['ghi'], window_length=60)
|
|
679
679
|
|
|
680
680
|
|
|
681
|
-
|
|
681
|
+
@pytest.mark.parametrize("window_length", [5, 10, 15, 20, 25])
|
|
682
|
+
def test_detect_clearsky_optimizer_not_failed(
|
|
683
|
+
detect_clearsky_data, window_length
|
|
684
|
+
):
|
|
682
685
|
expected, cs = detect_clearsky_data
|
|
683
|
-
|
|
684
|
-
|
|
686
|
+
clear_samples = clearsky.detect_clearsky(
|
|
687
|
+
expected["GHI"], cs["ghi"], window_length=window_length
|
|
688
|
+
)
|
|
689
|
+
assert isinstance(clear_samples, pd.Series)
|
|
685
690
|
|
|
686
691
|
|
|
687
692
|
@pytest.fixture
|
|
@@ -749,6 +754,7 @@ def test_bird():
|
|
|
749
754
|
tz = -7 # test timezone
|
|
750
755
|
gmt_tz = pytz.timezone('Etc/GMT%+d' % -(tz))
|
|
751
756
|
times = times.tz_localize(gmt_tz) # set timezone
|
|
757
|
+
times_utc = times.tz_convert('UTC')
|
|
752
758
|
# match test data from BIRD_08_16_2012.xls
|
|
753
759
|
latitude = 40.
|
|
754
760
|
longitude = -105.
|
|
@@ -759,9 +765,9 @@ def test_bird():
|
|
|
759
765
|
aod_380nm = 0.15
|
|
760
766
|
b_a = 0.85
|
|
761
767
|
alb = 0.2
|
|
762
|
-
eot = solarposition.equation_of_time_spencer71(
|
|
768
|
+
eot = solarposition.equation_of_time_spencer71(times_utc.dayofyear)
|
|
763
769
|
hour_angle = solarposition.hour_angle(times, longitude, eot) - 0.5 * 15.
|
|
764
|
-
declination = solarposition.declination_spencer71(
|
|
770
|
+
declination = solarposition.declination_spencer71(times_utc.dayofyear)
|
|
765
771
|
zenith = solarposition.solar_zenith_analytical(
|
|
766
772
|
np.deg2rad(latitude), np.deg2rad(hour_angle), declination
|
|
767
773
|
)
|
|
@@ -777,32 +783,35 @@ def test_bird():
|
|
|
777
783
|
Eb, Ebh, Gh, Dh = (irrads[_] for _ in field_names)
|
|
778
784
|
data_path = DATA_DIR / 'BIRD_08_16_2012.csv'
|
|
779
785
|
testdata = pd.read_csv(data_path, usecols=range(1, 26), header=1).dropna()
|
|
780
|
-
testdata
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
assert np.allclose(testdata['
|
|
786
|
+
testdata[['DEC', 'EQT']] = testdata[['DEC', 'EQT']].shift(tz)
|
|
787
|
+
testdata = testdata[:tz]
|
|
788
|
+
end = 48 + tz
|
|
789
|
+
testdata.index = times[1:end]
|
|
790
|
+
assert np.allclose(testdata['DEC'], np.rad2deg(declination[1:end]))
|
|
791
|
+
assert np.allclose(testdata['EQT'], eot[1:end], rtol=1e-4)
|
|
792
|
+
assert np.allclose(testdata['Hour Angle'], hour_angle[1:end], rtol=1e-2)
|
|
793
|
+
assert np.allclose(testdata['Zenith Ang'], zenith[1:end], rtol=1e-2)
|
|
785
794
|
dawn = zenith < 88.
|
|
786
795
|
dusk = testdata['Zenith Ang'] < 88.
|
|
787
796
|
am = pd.Series(np.where(dawn, airmass, 0.), index=times).fillna(0.0)
|
|
788
797
|
assert np.allclose(
|
|
789
|
-
testdata['Air Mass'].where(dusk, 0.), am[1:
|
|
798
|
+
testdata['Air Mass'].where(dusk, 0.), am[1:end], rtol=1e-3
|
|
790
799
|
)
|
|
791
800
|
direct_beam = pd.Series(np.where(dawn, Eb, 0.), index=times).fillna(0.)
|
|
792
801
|
assert np.allclose(
|
|
793
|
-
testdata['Direct Beam'].where(dusk, 0.), direct_beam[1:
|
|
802
|
+
testdata['Direct Beam'].where(dusk, 0.), direct_beam[1:end], rtol=1e-3
|
|
794
803
|
)
|
|
795
804
|
direct_horz = pd.Series(np.where(dawn, Ebh, 0.), index=times).fillna(0.)
|
|
796
805
|
assert np.allclose(
|
|
797
|
-
testdata['Direct Hz'].where(dusk, 0.), direct_horz[1:
|
|
806
|
+
testdata['Direct Hz'].where(dusk, 0.), direct_horz[1:end], rtol=1e-3
|
|
798
807
|
)
|
|
799
808
|
global_horz = pd.Series(np.where(dawn, Gh, 0.), index=times).fillna(0.)
|
|
800
809
|
assert np.allclose(
|
|
801
|
-
testdata['Global Hz'].where(dusk, 0.), global_horz[1:
|
|
810
|
+
testdata['Global Hz'].where(dusk, 0.), global_horz[1:end], rtol=1e-3
|
|
802
811
|
)
|
|
803
812
|
diffuse_horz = pd.Series(np.where(dawn, Dh, 0.), index=times).fillna(0.)
|
|
804
813
|
assert np.allclose(
|
|
805
|
-
testdata['Dif Hz'].where(dusk, 0.), diffuse_horz[1:
|
|
814
|
+
testdata['Dif Hz'].where(dusk, 0.), diffuse_horz[1:end], rtol=1e-3
|
|
806
815
|
)
|
|
807
816
|
# repeat test with albedo as a Series
|
|
808
817
|
alb_series = pd.Series(0.2, index=times)
|
|
@@ -813,19 +822,19 @@ def test_bird():
|
|
|
813
822
|
Eb, Ebh, Gh, Dh = (irrads[_] for _ in field_names)
|
|
814
823
|
direct_beam = pd.Series(np.where(dawn, Eb, 0.), index=times).fillna(0.)
|
|
815
824
|
assert np.allclose(
|
|
816
|
-
testdata['Direct Beam'].where(dusk, 0.), direct_beam[1:
|
|
825
|
+
testdata['Direct Beam'].where(dusk, 0.), direct_beam[1:end], rtol=1e-3
|
|
817
826
|
)
|
|
818
827
|
direct_horz = pd.Series(np.where(dawn, Ebh, 0.), index=times).fillna(0.)
|
|
819
828
|
assert np.allclose(
|
|
820
|
-
testdata['Direct Hz'].where(dusk, 0.), direct_horz[1:
|
|
829
|
+
testdata['Direct Hz'].where(dusk, 0.), direct_horz[1:end], rtol=1e-3
|
|
821
830
|
)
|
|
822
831
|
global_horz = pd.Series(np.where(dawn, Gh, 0.), index=times).fillna(0.)
|
|
823
832
|
assert np.allclose(
|
|
824
|
-
testdata['Global Hz'].where(dusk, 0.), global_horz[1:
|
|
833
|
+
testdata['Global Hz'].where(dusk, 0.), global_horz[1:end], rtol=1e-3
|
|
825
834
|
)
|
|
826
835
|
diffuse_horz = pd.Series(np.where(dawn, Dh, 0.), index=times).fillna(0.)
|
|
827
836
|
assert np.allclose(
|
|
828
|
-
testdata['Dif Hz'].where(dusk, 0.), diffuse_horz[1:
|
|
837
|
+
testdata['Dif Hz'].where(dusk, 0.), diffuse_horz[1:end], rtol=1e-3
|
|
829
838
|
)
|
|
830
839
|
|
|
831
840
|
# test keyword parameters
|
|
@@ -835,22 +844,25 @@ def test_bird():
|
|
|
835
844
|
Eb2, Ebh2, Gh2, Dh2 = (irrads2[_] for _ in field_names)
|
|
836
845
|
data_path = DATA_DIR / 'BIRD_08_16_2012_patm.csv'
|
|
837
846
|
testdata2 = pd.read_csv(data_path, usecols=range(1, 26), header=1).dropna()
|
|
838
|
-
testdata2
|
|
847
|
+
testdata2[['DEC', 'EQT']] = testdata2[['DEC', 'EQT']].shift(tz)
|
|
848
|
+
testdata2 = testdata2[:tz]
|
|
849
|
+
testdata2.index = times[1:end]
|
|
839
850
|
direct_beam2 = pd.Series(np.where(dawn, Eb2, 0.), index=times).fillna(0.)
|
|
840
851
|
assert np.allclose(
|
|
841
|
-
testdata2['Direct Beam'].where(dusk, 0.), direct_beam2[1:
|
|
852
|
+
testdata2['Direct Beam'].where(dusk, 0.), direct_beam2[1:end],
|
|
853
|
+
rtol=1e-3
|
|
842
854
|
)
|
|
843
855
|
direct_horz2 = pd.Series(np.where(dawn, Ebh2, 0.), index=times).fillna(0.)
|
|
844
856
|
assert np.allclose(
|
|
845
|
-
testdata2['Direct Hz'].where(dusk, 0.), direct_horz2[1:
|
|
857
|
+
testdata2['Direct Hz'].where(dusk, 0.), direct_horz2[1:end], rtol=1e-3
|
|
846
858
|
)
|
|
847
859
|
global_horz2 = pd.Series(np.where(dawn, Gh2, 0.), index=times).fillna(0.)
|
|
848
860
|
assert np.allclose(
|
|
849
|
-
testdata2['Global Hz'].where(dusk, 0.), global_horz2[1:
|
|
861
|
+
testdata2['Global Hz'].where(dusk, 0.), global_horz2[1:end], rtol=1e-3
|
|
850
862
|
)
|
|
851
863
|
diffuse_horz2 = pd.Series(np.where(dawn, Dh2, 0.), index=times).fillna(0.)
|
|
852
864
|
assert np.allclose(
|
|
853
|
-
testdata2['Dif Hz'].where(dusk, 0.), diffuse_horz2[1:
|
|
865
|
+
testdata2['Dif Hz'].where(dusk, 0.), diffuse_horz2[1:end], rtol=1e-3
|
|
854
866
|
)
|
|
855
867
|
# test scalars just at noon
|
|
856
868
|
# XXX: calculations start at 12am so noon is at index = 12
|
pvlib/tests/test_irradiance.py
CHANGED
|
@@ -59,7 +59,7 @@ def ephem_data(times):
|
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
@pytest.fixture
|
|
62
|
-
def dni_et(
|
|
62
|
+
def dni_et():
|
|
63
63
|
return np.array(
|
|
64
64
|
[1321.1655834833093, 1321.1655834833093, 1321.1655834833093,
|
|
65
65
|
1321.1655834833093])
|
|
@@ -106,7 +106,7 @@ def test_get_extra_radiation_epoch_year():
|
|
|
106
106
|
@requires_numba
|
|
107
107
|
def test_get_extra_radiation_nrel_numba(times):
|
|
108
108
|
with warnings.catch_warnings():
|
|
109
|
-
# don't warn on method reload
|
|
109
|
+
# don't warn on method reload
|
|
110
110
|
warnings.simplefilter("ignore")
|
|
111
111
|
result = irradiance.get_extra_radiation(
|
|
112
112
|
times, method='nrel', how='numba', numthreads=4)
|
|
@@ -603,11 +603,11 @@ def test_poa_components(irrad_data, ephem_data, dni_et, relative_airmass):
|
|
|
603
603
|
|
|
604
604
|
@pytest.mark.parametrize('pressure,expected', [
|
|
605
605
|
(93193, [[830.46567, 0.79742, 0.93505],
|
|
606
|
-
[676.
|
|
606
|
+
[676.18340, 0.63782, 3.02102]]),
|
|
607
607
|
(None, [[868.72425, 0.79742, 1.01664],
|
|
608
|
-
[680.
|
|
608
|
+
[680.73800, 0.63782, 3.28463]]),
|
|
609
609
|
(101325, [[868.72425, 0.79742, 1.01664],
|
|
610
|
-
[680.
|
|
610
|
+
[680.73800, 0.63782, 3.28463]])
|
|
611
611
|
])
|
|
612
612
|
def test_disc_value(pressure, expected):
|
|
613
613
|
# see GH 449 for pressure=None vs. 101325.
|
|
@@ -1080,7 +1080,7 @@ def test_dirindex(times):
|
|
|
1080
1080
|
pressure=pressure,
|
|
1081
1081
|
use_delta_kt_prime=True,
|
|
1082
1082
|
temp_dew=tdew).values
|
|
1083
|
-
expected_out = np.array([np.nan, 0., 748.
|
|
1083
|
+
expected_out = np.array([np.nan, 0., 748.31562800, 630.73752100])
|
|
1084
1084
|
|
|
1085
1085
|
tolerance = 1e-8
|
|
1086
1086
|
assert np.allclose(out, expected_out, rtol=tolerance, atol=0,
|
|
@@ -139,7 +139,8 @@ def test_spa_python_numpy_physical_dst(expected_solpos, golden):
|
|
|
139
139
|
assert_frame_equal(expected_solpos, ephem_data[expected_solpos.columns])
|
|
140
140
|
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
@pytest.mark.parametrize('delta_t', [65.0, None, np.array([65, 65])])
|
|
143
|
+
def test_sun_rise_set_transit_spa(expected_rise_set_spa, golden, delta_t):
|
|
143
144
|
# solution from NREL SAP web calculator
|
|
144
145
|
south = Location(-35.0, 0.0, tz='UTC')
|
|
145
146
|
times = pd.DatetimeIndex([datetime.datetime(1996, 7, 5, 0),
|
|
@@ -160,7 +161,7 @@ def test_sun_rise_set_transit_spa(expected_rise_set_spa, golden):
|
|
|
160
161
|
|
|
161
162
|
result = solarposition.sun_rise_set_transit_spa(times, south.latitude,
|
|
162
163
|
south.longitude,
|
|
163
|
-
delta_t=
|
|
164
|
+
delta_t=delta_t)
|
|
164
165
|
result_rounded = pd.DataFrame(index=result.index)
|
|
165
166
|
# need to iterate because to_datetime does not accept 2D data
|
|
166
167
|
# the rounding fails on pandas < 0.17
|
|
@@ -172,7 +173,7 @@ def test_sun_rise_set_transit_spa(expected_rise_set_spa, golden):
|
|
|
172
173
|
# test for Golden, CO compare to NREL SPA
|
|
173
174
|
result = solarposition.sun_rise_set_transit_spa(
|
|
174
175
|
expected_rise_set_spa.index, golden.latitude, golden.longitude,
|
|
175
|
-
delta_t=
|
|
176
|
+
delta_t=delta_t)
|
|
176
177
|
|
|
177
178
|
# round to nearest minute
|
|
178
179
|
result_rounded = pd.DataFrame(index=result.index)
|
|
@@ -477,20 +478,20 @@ def test_get_solarposition_altitude(
|
|
|
477
478
|
|
|
478
479
|
|
|
479
480
|
@pytest.mark.parametrize("delta_t, method", [
|
|
480
|
-
(None, '
|
|
481
|
-
pytest.param(
|
|
482
|
-
None, 'nrel_numba',
|
|
483
|
-
marks=[pytest.mark.xfail(
|
|
484
|
-
reason='spa.calculate_deltat not implemented for numba yet')]),
|
|
481
|
+
(None, 'nrel_numba'),
|
|
485
482
|
(67.0, 'nrel_numba'),
|
|
483
|
+
(np.array([67.0, 67.0]), 'nrel_numba'),
|
|
484
|
+
# minimize reloads, with numpy being last
|
|
485
|
+
(None, 'nrel_numpy'),
|
|
486
486
|
(67.0, 'nrel_numpy'),
|
|
487
|
-
])
|
|
487
|
+
(np.array([67.0, 67.0]), 'nrel_numpy'),
|
|
488
|
+
])
|
|
488
489
|
def test_get_solarposition_deltat(delta_t, method, expected_solpos_multi,
|
|
489
490
|
golden):
|
|
490
491
|
times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30),
|
|
491
492
|
periods=2, freq='D', tz=golden.tz)
|
|
492
493
|
with warnings.catch_warnings():
|
|
493
|
-
# don't warn on method reload
|
|
494
|
+
# don't warn on method reload
|
|
494
495
|
warnings.simplefilter("ignore")
|
|
495
496
|
ephem_data = solarposition.get_solarposition(times, golden.latitude,
|
|
496
497
|
golden.longitude,
|
|
@@ -505,6 +506,21 @@ def test_get_solarposition_deltat(delta_t, method, expected_solpos_multi,
|
|
|
505
506
|
assert_frame_equal(this_expected, ephem_data[this_expected.columns])
|
|
506
507
|
|
|
507
508
|
|
|
509
|
+
@pytest.mark.parametrize("method", ['nrel_numba', 'nrel_numpy'])
|
|
510
|
+
def test_spa_array_delta_t(method):
|
|
511
|
+
# make sure that time-varying delta_t produces different answers
|
|
512
|
+
times = pd.to_datetime(["2019-01-01", "2019-01-01"]).tz_localize("UTC")
|
|
513
|
+
expected = pd.Series([257.26969492, 257.2701359], index=times)
|
|
514
|
+
with warnings.catch_warnings():
|
|
515
|
+
# don't warn on method reload
|
|
516
|
+
warnings.simplefilter("ignore")
|
|
517
|
+
ephem_data = solarposition.get_solarposition(times, 40, -80,
|
|
518
|
+
delta_t=np.array([67, 0]),
|
|
519
|
+
method=method)
|
|
520
|
+
|
|
521
|
+
assert_series_equal(ephem_data['azimuth'], expected, check_names=False)
|
|
522
|
+
|
|
523
|
+
|
|
508
524
|
def test_get_solarposition_no_kwargs(expected_solpos, golden):
|
|
509
525
|
times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30),
|
|
510
526
|
periods=1, freq='D', tz=golden.tz)
|
|
@@ -529,20 +545,22 @@ def test_get_solarposition_method_pyephem(expected_solpos, golden):
|
|
|
529
545
|
assert_frame_equal(expected_solpos, ephem_data[expected_solpos.columns])
|
|
530
546
|
|
|
531
547
|
|
|
532
|
-
|
|
548
|
+
@pytest.mark.parametrize('delta_t', [64.0, None, np.array([64, 64])])
|
|
549
|
+
def test_nrel_earthsun_distance(delta_t):
|
|
533
550
|
times = pd.DatetimeIndex([datetime.datetime(2015, 1, 2),
|
|
534
551
|
datetime.datetime(2015, 8, 2)]
|
|
535
552
|
).tz_localize('MST')
|
|
536
|
-
result = solarposition.nrel_earthsun_distance(times, delta_t=
|
|
553
|
+
result = solarposition.nrel_earthsun_distance(times, delta_t=delta_t)
|
|
537
554
|
expected = pd.Series(np.array([0.983289204601, 1.01486146446]),
|
|
538
555
|
index=times)
|
|
539
556
|
assert_series_equal(expected, result)
|
|
540
557
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
558
|
+
if np.size(delta_t) == 1: # skip the array delta_t
|
|
559
|
+
times = datetime.datetime(2015, 1, 2)
|
|
560
|
+
result = solarposition.nrel_earthsun_distance(times, delta_t=delta_t)
|
|
561
|
+
expected = pd.Series(np.array([0.983289204601]),
|
|
562
|
+
index=pd.DatetimeIndex([times, ]))
|
|
563
|
+
assert_series_equal(expected, result)
|
|
546
564
|
|
|
547
565
|
|
|
548
566
|
def test_equation_of_time():
|
|
@@ -579,19 +597,20 @@ def test_declination():
|
|
|
579
597
|
def test_analytical_zenith():
|
|
580
598
|
times = pd.date_range(start="1/1/2015 0:00", end="12/31/2015 23:00",
|
|
581
599
|
freq="h").tz_localize('Etc/GMT+8')
|
|
600
|
+
times_utc = times.tz_convert('UTC')
|
|
582
601
|
lat, lon = 37.8, -122.25
|
|
583
602
|
lat_rad = np.deg2rad(lat)
|
|
584
603
|
output = solarposition.spa_python(times, lat, lon, 100)
|
|
585
604
|
solar_zenith = np.deg2rad(output['zenith']) # spa
|
|
586
605
|
# spencer
|
|
587
|
-
eot = solarposition.equation_of_time_spencer71(
|
|
606
|
+
eot = solarposition.equation_of_time_spencer71(times_utc.dayofyear)
|
|
588
607
|
hour_angle = np.deg2rad(solarposition.hour_angle(times, lon, eot))
|
|
589
|
-
decl = solarposition.declination_spencer71(
|
|
608
|
+
decl = solarposition.declination_spencer71(times_utc.dayofyear)
|
|
590
609
|
zenith_1 = solarposition.solar_zenith_analytical(lat_rad, hour_angle, decl)
|
|
591
610
|
# pvcdrom and cooper
|
|
592
|
-
eot = solarposition.equation_of_time_pvcdrom(
|
|
611
|
+
eot = solarposition.equation_of_time_pvcdrom(times_utc.dayofyear)
|
|
593
612
|
hour_angle = np.deg2rad(solarposition.hour_angle(times, lon, eot))
|
|
594
|
-
decl = solarposition.declination_cooper69(
|
|
613
|
+
decl = solarposition.declination_cooper69(times_utc.dayofyear)
|
|
595
614
|
zenith_2 = solarposition.solar_zenith_analytical(lat_rad, hour_angle, decl)
|
|
596
615
|
assert np.allclose(zenith_1, solar_zenith, atol=0.015)
|
|
597
616
|
assert np.allclose(zenith_2, solar_zenith, atol=0.025)
|
|
@@ -600,22 +619,23 @@ def test_analytical_zenith():
|
|
|
600
619
|
def test_analytical_azimuth():
|
|
601
620
|
times = pd.date_range(start="1/1/2015 0:00", end="12/31/2015 23:00",
|
|
602
621
|
freq="h").tz_localize('Etc/GMT+8')
|
|
622
|
+
times_utc = times.tz_convert('UTC')
|
|
603
623
|
lat, lon = 37.8, -122.25
|
|
604
624
|
lat_rad = np.deg2rad(lat)
|
|
605
625
|
output = solarposition.spa_python(times, lat, lon, 100)
|
|
606
626
|
solar_azimuth = np.deg2rad(output['azimuth']) # spa
|
|
607
627
|
solar_zenith = np.deg2rad(output['zenith'])
|
|
608
628
|
# spencer
|
|
609
|
-
eot = solarposition.equation_of_time_spencer71(
|
|
629
|
+
eot = solarposition.equation_of_time_spencer71(times_utc.dayofyear)
|
|
610
630
|
hour_angle = np.deg2rad(solarposition.hour_angle(times, lon, eot))
|
|
611
|
-
decl = solarposition.declination_spencer71(
|
|
631
|
+
decl = solarposition.declination_spencer71(times_utc.dayofyear)
|
|
612
632
|
zenith = solarposition.solar_zenith_analytical(lat_rad, hour_angle, decl)
|
|
613
633
|
azimuth_1 = solarposition.solar_azimuth_analytical(lat_rad, hour_angle,
|
|
614
634
|
decl, zenith)
|
|
615
635
|
# pvcdrom and cooper
|
|
616
|
-
eot = solarposition.equation_of_time_pvcdrom(
|
|
636
|
+
eot = solarposition.equation_of_time_pvcdrom(times_utc.dayofyear)
|
|
617
637
|
hour_angle = np.deg2rad(solarposition.hour_angle(times, lon, eot))
|
|
618
|
-
decl = solarposition.declination_cooper69(
|
|
638
|
+
decl = solarposition.declination_cooper69(times_utc.dayofyear)
|
|
619
639
|
zenith = solarposition.solar_zenith_analytical(lat_rad, hour_angle, decl)
|
|
620
640
|
azimuth_2 = solarposition.solar_azimuth_analytical(lat_rad, hour_angle,
|
|
621
641
|
decl, zenith)
|
|
@@ -665,21 +685,45 @@ def test_hour_angle():
|
|
|
665
685
|
'2015-01-02 12:04:44.6340'
|
|
666
686
|
]).tz_localize('Etc/GMT+7')
|
|
667
687
|
eot = np.array([-3.935172, -4.117227, -4.026295])
|
|
668
|
-
|
|
688
|
+
hourangle = solarposition.hour_angle(times, longitude, eot)
|
|
669
689
|
expected = (-70.682338, 70.72118825000001, 0.000801250)
|
|
670
690
|
# FIXME: there are differences from expected NREL SPA calculator values
|
|
671
691
|
# sunrise: 4 seconds, sunset: 48 seconds, transit: 0.2 seconds
|
|
672
692
|
# but the differences may be due to other SPA input parameters
|
|
673
|
-
assert np.allclose(
|
|
693
|
+
assert np.allclose(hourangle, expected)
|
|
694
|
+
|
|
695
|
+
hours = solarposition._hour_angle_to_hours(
|
|
696
|
+
times, hourangle, longitude, eot)
|
|
697
|
+
result = solarposition._times_to_hours_after_local_midnight(times)
|
|
698
|
+
assert np.allclose(result, hours)
|
|
699
|
+
|
|
700
|
+
result = solarposition._local_times_from_hours_since_midnight(times, hours)
|
|
701
|
+
assert result.equals(times)
|
|
702
|
+
|
|
703
|
+
times = times.tz_convert(None)
|
|
704
|
+
with pytest.raises(ValueError):
|
|
705
|
+
solarposition.hour_angle(times, longitude, eot)
|
|
706
|
+
with pytest.raises(ValueError):
|
|
707
|
+
solarposition._hour_angle_to_hours(times, hourangle, longitude, eot)
|
|
708
|
+
with pytest.raises(ValueError):
|
|
709
|
+
solarposition._times_to_hours_after_local_midnight(times)
|
|
710
|
+
with pytest.raises(ValueError):
|
|
711
|
+
solarposition._local_times_from_hours_since_midnight(times, hours)
|
|
674
712
|
|
|
675
713
|
|
|
676
714
|
def test_sun_rise_set_transit_geometric(expected_rise_set_spa, golden_mst):
|
|
677
715
|
"""Test geometric calculations for sunrise, sunset, and transit times"""
|
|
678
716
|
times = expected_rise_set_spa.index
|
|
717
|
+
times_utc = times.tz_convert('UTC')
|
|
679
718
|
latitude = golden_mst.latitude
|
|
680
719
|
longitude = golden_mst.longitude
|
|
681
|
-
eot = solarposition.equation_of_time_spencer71(
|
|
682
|
-
|
|
720
|
+
eot = solarposition.equation_of_time_spencer71(
|
|
721
|
+
times_utc.dayofyear) # minutes
|
|
722
|
+
decl = solarposition.declination_spencer71(times_utc.dayofyear) # radians
|
|
723
|
+
with pytest.raises(ValueError):
|
|
724
|
+
solarposition.sun_rise_set_transit_geometric(
|
|
725
|
+
times.tz_convert(None), latitude=latitude, longitude=longitude,
|
|
726
|
+
declination=decl, equation_of_time=eot)
|
|
683
727
|
sr, ss, st = solarposition.sun_rise_set_transit_geometric(
|
|
684
728
|
times, latitude=latitude, longitude=longitude, declination=decl,
|
|
685
729
|
equation_of_time=eot)
|
|
@@ -742,6 +786,7 @@ def test__datetime_to_unixtime_units(unit, tz):
|
|
|
742
786
|
|
|
743
787
|
|
|
744
788
|
@requires_pandas_2_0
|
|
789
|
+
@pytest.mark.parametrize('tz', [None, 'utc', 'US/Eastern'])
|
|
745
790
|
@pytest.mark.parametrize('method', [
|
|
746
791
|
'nrel_numpy',
|
|
747
792
|
'ephemeris',
|
|
@@ -749,7 +794,6 @@ def test__datetime_to_unixtime_units(unit, tz):
|
|
|
749
794
|
pytest.param('nrel_numba', marks=requires_numba),
|
|
750
795
|
pytest.param('nrel_c', marks=requires_spa_c),
|
|
751
796
|
])
|
|
752
|
-
@pytest.mark.parametrize('tz', [None, 'utc', 'US/Eastern'])
|
|
753
797
|
def test_get_solarposition_microsecond_index(method, tz):
|
|
754
798
|
# https://github.com/pvlib/pvlib-python/issues/1932
|
|
755
799
|
|
|
@@ -758,8 +802,12 @@ def test_get_solarposition_microsecond_index(method, tz):
|
|
|
758
802
|
index_ns = pd.date_range(unit='ns', **kwargs)
|
|
759
803
|
index_us = pd.date_range(unit='us', **kwargs)
|
|
760
804
|
|
|
761
|
-
|
|
762
|
-
|
|
805
|
+
with warnings.catch_warnings():
|
|
806
|
+
# don't warn on method reload
|
|
807
|
+
warnings.simplefilter("ignore")
|
|
808
|
+
|
|
809
|
+
sp_ns = solarposition.get_solarposition(index_ns, 0, 0, method=method)
|
|
810
|
+
sp_us = solarposition.get_solarposition(index_us, 0, 0, method=method)
|
|
763
811
|
|
|
764
812
|
assert_frame_equal(sp_ns, sp_us, check_index_type=False)
|
|
765
813
|
|
|
@@ -781,7 +829,7 @@ def test_nrel_earthsun_distance_microsecond_index(tz):
|
|
|
781
829
|
|
|
782
830
|
|
|
783
831
|
@requires_pandas_2_0
|
|
784
|
-
@pytest.mark.parametrize('tz', [
|
|
832
|
+
@pytest.mark.parametrize('tz', ['utc', 'US/Eastern'])
|
|
785
833
|
def test_hour_angle_microsecond_index(tz):
|
|
786
834
|
# https://github.com/pvlib/pvlib-python/issues/1932
|
|
787
835
|
|
|
@@ -813,7 +861,7 @@ def test_rise_set_transit_spa_microsecond_index(tz):
|
|
|
813
861
|
|
|
814
862
|
|
|
815
863
|
@requires_pandas_2_0
|
|
816
|
-
@pytest.mark.parametrize('tz', [
|
|
864
|
+
@pytest.mark.parametrize('tz', ['utc', 'US/Eastern'])
|
|
817
865
|
def test_rise_set_transit_geometric_microsecond_index(tz):
|
|
818
866
|
# https://github.com/pvlib/pvlib-python/issues/1932
|
|
819
867
|
|
|
@@ -838,7 +886,7 @@ def test_spa_python_numba_physical(expected_solpos, golden_mst):
|
|
|
838
886
|
times = pd.date_range(datetime.datetime(2003, 10, 17, 12, 30, 30),
|
|
839
887
|
periods=1, freq='D', tz=golden_mst.tz)
|
|
840
888
|
with warnings.catch_warnings():
|
|
841
|
-
# don't warn on method reload
|
|
889
|
+
# don't warn on method reload
|
|
842
890
|
# ensure that numpy is the most recently used method so that
|
|
843
891
|
# we can use the warns filter below
|
|
844
892
|
warnings.simplefilter("ignore")
|
|
@@ -865,7 +913,7 @@ def test_spa_python_numba_physical_dst(expected_solpos, golden):
|
|
|
865
913
|
periods=1, freq='D', tz=golden.tz)
|
|
866
914
|
|
|
867
915
|
with warnings.catch_warnings():
|
|
868
|
-
# don't warn on method reload
|
|
916
|
+
# don't warn on method reload
|
|
869
917
|
warnings.simplefilter("ignore")
|
|
870
918
|
ephem_data = solarposition.spa_python(times, golden.latitude,
|
|
871
919
|
golden.longitude, pressure=82000,
|
pvlib/tests/test_spa.py
CHANGED
|
@@ -234,7 +234,7 @@ class SpaBase:
|
|
|
234
234
|
|
|
235
235
|
def test_solar_position(self):
|
|
236
236
|
with warnings.catch_warnings():
|
|
237
|
-
# don't warn on method reload
|
|
237
|
+
# don't warn on method reload
|
|
238
238
|
warnings.simplefilter("ignore")
|
|
239
239
|
spa_out_0 = self.spa.solar_position(
|
|
240
240
|
unixtimes, lat, lon, elev, pressure, temp, delta_t,
|
pvlib/tools.py
CHANGED
|
@@ -206,8 +206,32 @@ def _pandas_to_doy(pd_object):
|
|
|
206
206
|
Returns
|
|
207
207
|
-------
|
|
208
208
|
dayofyear
|
|
209
|
+
|
|
210
|
+
Notes
|
|
211
|
+
-----
|
|
212
|
+
Day of year is determined using UTC, since pandas uses local hour
|
|
213
|
+
"""
|
|
214
|
+
return _pandas_to_utc(pd_object).dayofyear
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _pandas_to_utc(pd_object):
|
|
209
218
|
"""
|
|
210
|
-
|
|
219
|
+
Converts a pandas datetime-like object to UTC, if localized.
|
|
220
|
+
Otherwise, assume UTC.
|
|
221
|
+
|
|
222
|
+
Parameters
|
|
223
|
+
----------
|
|
224
|
+
pd_object : DatetimeIndex or Timestamp
|
|
225
|
+
|
|
226
|
+
Returns
|
|
227
|
+
-------
|
|
228
|
+
pandas object localized to or assumed to be UTC.
|
|
229
|
+
"""
|
|
230
|
+
try:
|
|
231
|
+
pd_object_utc = pd_object.tz_convert('UTC')
|
|
232
|
+
except TypeError:
|
|
233
|
+
pd_object_utc = pd_object
|
|
234
|
+
return pd_object_utc
|
|
211
235
|
|
|
212
236
|
|
|
213
237
|
def _doy_to_datetimeindex(doy, epoch_year=2014):
|
|
@@ -230,7 +254,7 @@ def _doy_to_datetimeindex(doy, epoch_year=2014):
|
|
|
230
254
|
|
|
231
255
|
|
|
232
256
|
def _datetimelike_scalar_to_doy(time):
|
|
233
|
-
return
|
|
257
|
+
return _pandas_to_doy(_datetimelike_scalar_to_datetimeindex(time))
|
|
234
258
|
|
|
235
259
|
|
|
236
260
|
def _datetimelike_scalar_to_datetimeindex(time):
|