pvlib 0.11.0a1__py3-none-any.whl → 0.11.2__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.
Files changed (62) hide show
  1. pvlib/_deprecation.py +73 -0
  2. pvlib/atmosphere.py +236 -1
  3. pvlib/bifacial/__init__.py +4 -4
  4. pvlib/bifacial/loss_models.py +163 -0
  5. pvlib/clearsky.py +53 -51
  6. pvlib/data/pvgis_tmy_meta.json +32 -93
  7. pvlib/data/pvgis_tmy_test.csv +8761 -0
  8. pvlib/data/tmy_45.000_8.000_2005_2023.csv +8789 -0
  9. pvlib/data/tmy_45.000_8.000_2005_2023.epw +8768 -0
  10. pvlib/data/tmy_45.000_8.000_2005_2023.json +1 -0
  11. pvlib/data/tmy_45.000_8.000_2005_2023.txt +8761 -0
  12. pvlib/data/tmy_45.000_8.000_userhorizon.json +1 -1
  13. pvlib/iam.py +4 -4
  14. pvlib/iotools/midc.py +1 -1
  15. pvlib/iotools/pvgis.py +39 -13
  16. pvlib/irradiance.py +237 -173
  17. pvlib/ivtools/sdm.py +75 -52
  18. pvlib/location.py +5 -5
  19. pvlib/modelchain.py +1 -1
  20. pvlib/pvsystem.py +134 -86
  21. pvlib/shading.py +8 -8
  22. pvlib/singlediode.py +1 -1
  23. pvlib/solarposition.py +101 -80
  24. pvlib/spa.py +28 -24
  25. pvlib/spectrum/__init__.py +9 -4
  26. pvlib/spectrum/irradiance.py +273 -0
  27. pvlib/spectrum/mismatch.py +118 -508
  28. pvlib/spectrum/response.py +280 -0
  29. pvlib/spectrum/spectrl2.py +18 -17
  30. pvlib/temperature.py +49 -3
  31. pvlib/tests/bifacial/test_losses_models.py +54 -0
  32. pvlib/tests/iotools/test_pvgis.py +58 -12
  33. pvlib/tests/ivtools/test_sdm.py +23 -1
  34. pvlib/tests/spectrum/__init__.py +0 -0
  35. pvlib/tests/spectrum/conftest.py +40 -0
  36. pvlib/tests/spectrum/test_irradiance.py +138 -0
  37. pvlib/tests/{test_spectrum.py → spectrum/test_mismatch.py} +32 -306
  38. pvlib/tests/spectrum/test_response.py +124 -0
  39. pvlib/tests/spectrum/test_spectrl2.py +72 -0
  40. pvlib/tests/test__deprecation.py +97 -0
  41. pvlib/tests/test_atmosphere.py +218 -0
  42. pvlib/tests/test_clearsky.py +44 -26
  43. pvlib/tests/test_conftest.py +0 -44
  44. pvlib/tests/test_irradiance.py +62 -16
  45. pvlib/tests/test_pvsystem.py +17 -1
  46. pvlib/tests/test_solarposition.py +117 -36
  47. pvlib/tests/test_spa.py +30 -1
  48. pvlib/tools.py +26 -2
  49. pvlib/tracking.py +53 -47
  50. {pvlib-0.11.0a1.dist-info → pvlib-0.11.2.dist-info}/METADATA +34 -31
  51. {pvlib-0.11.0a1.dist-info → pvlib-0.11.2.dist-info}/RECORD +55 -47
  52. {pvlib-0.11.0a1.dist-info → pvlib-0.11.2.dist-info}/WHEEL +1 -1
  53. pvlib/data/aod550_tcwv_20121101_test.nc +0 -0
  54. pvlib/data/pvgis_tmy_test.dat +0 -8761
  55. pvlib/data/tmy_45.000_8.000_2005_2016.csv +0 -8789
  56. pvlib/data/tmy_45.000_8.000_2005_2016.epw +0 -8768
  57. pvlib/data/tmy_45.000_8.000_2005_2016.json +0 -1
  58. pvlib/data/tmy_45.000_8.000_2005_2016.txt +0 -8761
  59. pvlib/data/variables_style_rules.csv +0 -55
  60. {pvlib-0.11.0a1.dist-info → pvlib-0.11.2.dist-info}/AUTHORS.md +0 -0
  61. {pvlib-0.11.0a1.dist-info → pvlib-0.11.2.dist-info}/LICENSE +0 -0
  62. {pvlib-0.11.0a1.dist-info → pvlib-0.11.2.dist-info}/top_level.txt +0 -0
@@ -1,50 +1,6 @@
1
1
  import pytest
2
- import pandas
3
2
 
4
3
  from pvlib.tests import conftest
5
- from pvlib.tests.conftest import fail_on_pvlib_version
6
-
7
- from pvlib._deprecation import pvlibDeprecationWarning, deprecated
8
-
9
- @pytest.mark.xfail(strict=True,
10
- reason='fail_on_pvlib_version should cause test to fail')
11
- @fail_on_pvlib_version('0.0')
12
- def test_fail_on_pvlib_version():
13
- pass
14
-
15
-
16
- @fail_on_pvlib_version('100000.0')
17
- def test_fail_on_pvlib_version_pass():
18
- pass
19
-
20
-
21
- @pytest.mark.xfail(strict=True, reason='ensure that the test is called')
22
- @fail_on_pvlib_version('100000.0')
23
- def test_fail_on_pvlib_version_fail_in_test():
24
- raise Exception
25
-
26
-
27
- # set up to test using fixtures with function decorated with
28
- # conftest.fail_on_pvlib_version
29
- @pytest.fixture()
30
- def some_data():
31
- return "some data"
32
-
33
-
34
- def alt_func(*args):
35
- return args
36
-
37
-
38
- deprec_func = deprecated('350.8', alternative='alt_func',
39
- name='deprec_func', removal='350.9')(alt_func)
40
-
41
-
42
- @fail_on_pvlib_version('350.9')
43
- def test_use_fixture_with_decorator(some_data):
44
- # test that the correct data is returned by the some_data fixture
45
- assert some_data == "some data"
46
- with pytest.warns(pvlibDeprecationWarning): # test for deprecation warning
47
- deprec_func(some_data)
48
4
 
49
5
 
50
6
  @pytest.mark.parametrize('function_name', ['assert_index_equal',
@@ -15,7 +15,8 @@ from .conftest import (
15
15
  assert_frame_equal,
16
16
  assert_series_equal,
17
17
  requires_ephem,
18
- requires_numba
18
+ requires_numba,
19
+ fail_on_pvlib_version,
19
20
  )
20
21
 
21
22
  from pvlib._deprecation import pvlibDeprecationWarning
@@ -59,7 +60,7 @@ def ephem_data(times):
59
60
 
60
61
 
61
62
  @pytest.fixture
62
- def dni_et(times):
63
+ def dni_et():
63
64
  return np.array(
64
65
  [1321.1655834833093, 1321.1655834833093, 1321.1655834833093,
65
66
  1321.1655834833093])
@@ -106,7 +107,7 @@ def test_get_extra_radiation_epoch_year():
106
107
  @requires_numba
107
108
  def test_get_extra_radiation_nrel_numba(times):
108
109
  with warnings.catch_warnings():
109
- # don't warn on method reload or num threads
110
+ # don't warn on method reload
110
111
  warnings.simplefilter("ignore")
111
112
  result = irradiance.get_extra_radiation(
112
113
  times, method='nrel', how='numba', numthreads=4)
@@ -603,11 +604,11 @@ def test_poa_components(irrad_data, ephem_data, dni_et, relative_airmass):
603
604
 
604
605
  @pytest.mark.parametrize('pressure,expected', [
605
606
  (93193, [[830.46567, 0.79742, 0.93505],
606
- [676.09497, 0.63776, 3.02102]]),
607
+ [676.18340, 0.63782, 3.02102]]),
607
608
  (None, [[868.72425, 0.79742, 1.01664],
608
- [680.66679, 0.63776, 3.28463]]),
609
+ [680.73800, 0.63782, 3.28463]]),
609
610
  (101325, [[868.72425, 0.79742, 1.01664],
610
- [680.66679, 0.63776, 3.28463]])
611
+ [680.73800, 0.63782, 3.28463]])
611
612
  ])
612
613
  def test_disc_value(pressure, expected):
613
614
  # see GH 449 for pressure=None vs. 101325.
@@ -1063,7 +1064,7 @@ def test_dirindex(times):
1063
1064
  np.array([0., 79.73860422, 1042.48031487, 257.20751138]),
1064
1065
  index=times
1065
1066
  )
1066
- dni_clearsky = pd.Series(
1067
+ dni_clear = pd.Series(
1067
1068
  np.array([0., 316.1949056, 939.95469881, 646.22886049]),
1068
1069
  index=times
1069
1070
  )
@@ -1073,14 +1074,14 @@ def test_dirindex(times):
1073
1074
  )
1074
1075
  pressure = 93193.
1075
1076
  tdew = 10.
1076
- out = irradiance.dirindex(ghi, ghi_clearsky, dni_clearsky,
1077
+ out = irradiance.dirindex(ghi, ghi_clearsky, dni_clear,
1077
1078
  zenith, times, pressure=pressure,
1078
1079
  temp_dew=tdew)
1079
1080
  dirint_close_values = irradiance.dirint(ghi, zenith, times,
1080
1081
  pressure=pressure,
1081
1082
  use_delta_kt_prime=True,
1082
1083
  temp_dew=tdew).values
1083
- expected_out = np.array([np.nan, 0., 748.31562753, 630.72592644])
1084
+ expected_out = np.array([np.nan, 0., 748.31562800, 630.73752100])
1084
1085
 
1085
1086
  tolerance = 1e-8
1086
1087
  assert np.allclose(out, expected_out, rtol=tolerance, atol=0,
@@ -1094,6 +1095,20 @@ def test_dirindex(times):
1094
1095
  equal_nan=True)
1095
1096
 
1096
1097
 
1098
+ @fail_on_pvlib_version("0.13")
1099
+ def test_dirindex_ghi_clearsky_deprecation():
1100
+ times = pd.DatetimeIndex(['2014-06-24T18-1200'])
1101
+ ghi = pd.Series([1038.62], index=times)
1102
+ ghi_clearsky = pd.Series([1042.48031487], index=times)
1103
+ dni_clearsky = pd.Series([939.95469881], index=times)
1104
+ zenith = pd.Series([10.56413562], index=times)
1105
+ pressure, tdew = 93193, 10
1106
+ with pytest.warns(pvlibDeprecationWarning, match='ghi_clear'):
1107
+ irradiance.dirindex(
1108
+ ghi=ghi, ghi_clearsky=ghi_clearsky, dni_clear=dni_clearsky,
1109
+ zenith=zenith, times=times, pressure=pressure, temp_dew=tdew)
1110
+
1111
+
1097
1112
  def test_dirindex_min_cos_zenith_max_zenith():
1098
1113
  # map out behavior under difficult conditions with various
1099
1114
  # limiting kwargs settings
@@ -1101,38 +1116,51 @@ def test_dirindex_min_cos_zenith_max_zenith():
1101
1116
  times = pd.DatetimeIndex(['2014-06-24T12-0700', '2014-06-24T18-0700'])
1102
1117
  ghi = pd.Series([0, 1], index=times)
1103
1118
  ghi_clearsky = pd.Series([0, 1], index=times)
1104
- dni_clearsky = pd.Series([0, 5], index=times)
1119
+ dni_clear = pd.Series([0, 5], index=times)
1105
1120
  solar_zenith = pd.Series([90, 89.99], index=times)
1106
1121
 
1107
- out = irradiance.dirindex(ghi, ghi_clearsky, dni_clearsky, solar_zenith,
1122
+ out = irradiance.dirindex(ghi, ghi_clearsky, dni_clear, solar_zenith,
1108
1123
  times)
1109
1124
  expected = pd.Series([nan, nan], index=times)
1110
1125
  assert_series_equal(out, expected)
1111
1126
 
1112
- out = irradiance.dirindex(ghi, ghi_clearsky, dni_clearsky, solar_zenith,
1127
+ out = irradiance.dirindex(ghi, ghi_clearsky, dni_clear, solar_zenith,
1113
1128
  times, min_cos_zenith=0)
1114
1129
  expected = pd.Series([nan, nan], index=times)
1115
1130
  assert_series_equal(out, expected)
1116
1131
 
1117
- out = irradiance.dirindex(ghi, ghi_clearsky, dni_clearsky, solar_zenith,
1132
+ out = irradiance.dirindex(ghi, ghi_clearsky, dni_clear, solar_zenith,
1118
1133
  times, max_zenith=90)
1119
1134
  expected = pd.Series([nan, nan], index=times)
1120
1135
  assert_series_equal(out, expected)
1121
1136
 
1122
- out = irradiance.dirindex(ghi, ghi_clearsky, dni_clearsky, solar_zenith,
1137
+ out = irradiance.dirindex(ghi, ghi_clearsky, dni_clear, solar_zenith,
1123
1138
  times, min_cos_zenith=0, max_zenith=100)
1124
1139
  expected = pd.Series([nan, 5.], index=times)
1125
1140
  assert_series_equal(out, expected)
1126
1141
 
1127
1142
 
1143
+ @fail_on_pvlib_version("0.13")
1144
+ def test_dirindex_dni_clearsky_deprecation():
1145
+ times = pd.DatetimeIndex(['2014-06-24T12-0700', '2014-06-24T18-0700'])
1146
+ ghi = pd.Series([0, 1], index=times)
1147
+ ghi_clearsky = pd.Series([0, 1], index=times)
1148
+ dni_clear = pd.Series([0, 5], index=times)
1149
+ solar_zenith = pd.Series([90, 89.99], index=times)
1150
+ with pytest.warns(pvlibDeprecationWarning, match='dni_clear'):
1151
+ irradiance.dirindex(ghi, ghi_clearsky, dni_clearsky=dni_clear,
1152
+ zenith=solar_zenith, times=times,
1153
+ min_cos_zenith=0)
1154
+
1155
+
1128
1156
  def test_dni():
1129
1157
  ghi = pd.Series([90, 100, 100, 100, 100])
1130
1158
  dhi = pd.Series([100, 90, 50, 50, 50])
1131
1159
  zenith = pd.Series([80, 100, 85, 70, 85])
1132
- clearsky_dni = pd.Series([50, 50, 200, 50, 300])
1160
+ dni_clear = pd.Series([50, 50, 200, 50, 300])
1133
1161
 
1134
1162
  dni = irradiance.dni(ghi, dhi, zenith,
1135
- clearsky_dni=clearsky_dni, clearsky_tolerance=2)
1163
+ dni_clear=dni_clear, clearsky_tolerance=2)
1136
1164
  assert_series_equal(dni,
1137
1165
  pd.Series([float('nan'), float('nan'), 400,
1138
1166
  146.190220008, 573.685662283]))
@@ -1143,6 +1171,17 @@ def test_dni():
1143
1171
  146.190220008, 573.685662283]))
1144
1172
 
1145
1173
 
1174
+ @fail_on_pvlib_version("0.13")
1175
+ def test_dni_dni_clearsky_deprecation():
1176
+ ghi = pd.Series([90, 100, 100, 100, 100])
1177
+ dhi = pd.Series([100, 90, 50, 50, 50])
1178
+ zenith = pd.Series([80, 100, 85, 70, 85])
1179
+ dni_clear = pd.Series([50, 50, 200, 50, 300])
1180
+ with pytest.warns(pvlibDeprecationWarning, match='dni_clear'):
1181
+ irradiance.dni(ghi, dhi, zenith,
1182
+ clearsky_dni=dni_clear, clearsky_tolerance=2)
1183
+
1184
+
1146
1185
  @pytest.mark.parametrize(
1147
1186
  'surface_tilt,surface_azimuth,solar_zenith,' +
1148
1187
  'solar_azimuth,aoi_expected,aoi_proj_expected',
@@ -1235,6 +1274,13 @@ def test_clearsky_index():
1235
1274
  assert_series_equal(out, expected)
1236
1275
 
1237
1276
 
1277
+ @fail_on_pvlib_version("0.13")
1278
+ def test_clearsky_index_clearsky_ghi_deprecation():
1279
+ with pytest.warns(pvlibDeprecationWarning, match='ghi_clear'):
1280
+ ghi, clearsky_ghi = 200, 300
1281
+ irradiance.clearsky_index(ghi, clearsky_ghi=clearsky_ghi)
1282
+
1283
+
1238
1284
  def test_clearness_index():
1239
1285
  ghi = np.array([-1, 0, 1, 1000])
1240
1286
  solar_zenith = np.array([180, 90, 89.999, 0])
@@ -1870,7 +1870,6 @@ def test_PVSystem_get_irradiance(solar_pos):
1870
1870
  irrads['dni'],
1871
1871
  irrads['ghi'],
1872
1872
  irrads['dhi'])
1873
-
1874
1873
  expected = pd.DataFrame(data=np.array(
1875
1874
  [[883.65494055, 745.86141676, 137.79352379, 126.397131, 11.39639279],
1876
1875
  [0., -0., 0., 0., 0.]]),
@@ -1881,6 +1880,23 @@ def test_PVSystem_get_irradiance(solar_pos):
1881
1880
  assert_frame_equal(irradiance, expected, check_less_precise=2)
1882
1881
 
1883
1882
 
1883
+ def test_PVSystem_get_irradiance_float():
1884
+ system = pvsystem.PVSystem(surface_tilt=32, surface_azimuth=135)
1885
+ irrads = {'dni': 900., 'ghi': 600., 'dhi': 100.}
1886
+ zenith = 55.366831
1887
+ azimuth = 172.320038
1888
+ irradiance = system.get_irradiance(zenith,
1889
+ azimuth,
1890
+ irrads['dni'],
1891
+ irrads['ghi'],
1892
+ irrads['dhi'])
1893
+ expected = {'poa_global': 884.80903423, 'poa_direct': 745.84258835,
1894
+ 'poa_diffuse': 138.96644588, 'poa_sky_diffuse': 127.57005309,
1895
+ 'poa_ground_diffuse': 11.39639279}
1896
+ for k, v in irradiance.items():
1897
+ assert np.isclose(v, expected[k], rtol=1e-6)
1898
+
1899
+
1884
1900
  def test_PVSystem_get_irradiance_albedo(solar_pos):
1885
1901
  system = pvsystem.PVSystem(surface_tilt=32, surface_azimuth=135)
1886
1902
  irrads = pd.DataFrame({'dni': [900, 0], 'ghi': [600, 0], 'dhi': [100, 0],
@@ -8,6 +8,7 @@ import pandas as pd
8
8
  from .conftest import assert_frame_equal, assert_series_equal
9
9
  from numpy.testing import assert_allclose
10
10
  import pytest
11
+ import pytz
11
12
 
12
13
  from pvlib.location import Location
13
14
  from pvlib import solarposition, spa
@@ -139,7 +140,8 @@ def test_spa_python_numpy_physical_dst(expected_solpos, golden):
139
140
  assert_frame_equal(expected_solpos, ephem_data[expected_solpos.columns])
140
141
 
141
142
 
142
- def test_sun_rise_set_transit_spa(expected_rise_set_spa, golden):
143
+ @pytest.mark.parametrize('delta_t', [65.0, None, np.array([65, 65])])
144
+ def test_sun_rise_set_transit_spa(expected_rise_set_spa, golden, delta_t):
143
145
  # solution from NREL SAP web calculator
144
146
  south = Location(-35.0, 0.0, tz='UTC')
145
147
  times = pd.DatetimeIndex([datetime.datetime(1996, 7, 5, 0),
@@ -160,7 +162,7 @@ def test_sun_rise_set_transit_spa(expected_rise_set_spa, golden):
160
162
 
161
163
  result = solarposition.sun_rise_set_transit_spa(times, south.latitude,
162
164
  south.longitude,
163
- delta_t=65.0)
165
+ delta_t=delta_t)
164
166
  result_rounded = pd.DataFrame(index=result.index)
165
167
  # need to iterate because to_datetime does not accept 2D data
166
168
  # the rounding fails on pandas < 0.17
@@ -172,7 +174,7 @@ def test_sun_rise_set_transit_spa(expected_rise_set_spa, golden):
172
174
  # test for Golden, CO compare to NREL SPA
173
175
  result = solarposition.sun_rise_set_transit_spa(
174
176
  expected_rise_set_spa.index, golden.latitude, golden.longitude,
175
- delta_t=65.0)
177
+ delta_t=delta_t)
176
178
 
177
179
  # round to nearest minute
178
180
  result_rounded = pd.DataFrame(index=result.index)
@@ -477,20 +479,20 @@ def test_get_solarposition_altitude(
477
479
 
478
480
 
479
481
  @pytest.mark.parametrize("delta_t, method", [
480
- (None, 'nrel_numpy'),
481
- pytest.param(
482
- None, 'nrel_numba',
483
- marks=[pytest.mark.xfail(
484
- reason='spa.calculate_deltat not implemented for numba yet')]),
482
+ (None, 'nrel_numba'),
485
483
  (67.0, 'nrel_numba'),
484
+ (np.array([67.0, 67.0]), 'nrel_numba'),
485
+ # minimize reloads, with numpy being last
486
+ (None, 'nrel_numpy'),
486
487
  (67.0, 'nrel_numpy'),
487
- ])
488
+ (np.array([67.0, 67.0]), 'nrel_numpy'),
489
+ ])
488
490
  def test_get_solarposition_deltat(delta_t, method, expected_solpos_multi,
489
491
  golden):
490
492
  times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30),
491
493
  periods=2, freq='D', tz=golden.tz)
492
494
  with warnings.catch_warnings():
493
- # don't warn on method reload or num threads
495
+ # don't warn on method reload
494
496
  warnings.simplefilter("ignore")
495
497
  ephem_data = solarposition.get_solarposition(times, golden.latitude,
496
498
  golden.longitude,
@@ -505,6 +507,21 @@ def test_get_solarposition_deltat(delta_t, method, expected_solpos_multi,
505
507
  assert_frame_equal(this_expected, ephem_data[this_expected.columns])
506
508
 
507
509
 
510
+ @pytest.mark.parametrize("method", ['nrel_numba', 'nrel_numpy'])
511
+ def test_spa_array_delta_t(method):
512
+ # make sure that time-varying delta_t produces different answers
513
+ times = pd.to_datetime(["2019-01-01", "2019-01-01"]).tz_localize("UTC")
514
+ expected = pd.Series([257.26969492, 257.2701359], index=times)
515
+ with warnings.catch_warnings():
516
+ # don't warn on method reload
517
+ warnings.simplefilter("ignore")
518
+ ephem_data = solarposition.get_solarposition(times, 40, -80,
519
+ delta_t=np.array([67, 0]),
520
+ method=method)
521
+
522
+ assert_series_equal(ephem_data['azimuth'], expected, check_names=False)
523
+
524
+
508
525
  def test_get_solarposition_no_kwargs(expected_solpos, golden):
509
526
  times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30),
510
527
  periods=1, freq='D', tz=golden.tz)
@@ -529,20 +546,22 @@ def test_get_solarposition_method_pyephem(expected_solpos, golden):
529
546
  assert_frame_equal(expected_solpos, ephem_data[expected_solpos.columns])
530
547
 
531
548
 
532
- def test_nrel_earthsun_distance():
549
+ @pytest.mark.parametrize('delta_t', [64.0, None, np.array([64, 64])])
550
+ def test_nrel_earthsun_distance(delta_t):
533
551
  times = pd.DatetimeIndex([datetime.datetime(2015, 1, 2),
534
552
  datetime.datetime(2015, 8, 2)]
535
553
  ).tz_localize('MST')
536
- result = solarposition.nrel_earthsun_distance(times, delta_t=64.0)
554
+ result = solarposition.nrel_earthsun_distance(times, delta_t=delta_t)
537
555
  expected = pd.Series(np.array([0.983289204601, 1.01486146446]),
538
556
  index=times)
539
557
  assert_series_equal(expected, result)
540
558
 
541
- times = datetime.datetime(2015, 1, 2)
542
- result = solarposition.nrel_earthsun_distance(times, delta_t=64.0)
543
- expected = pd.Series(np.array([0.983289204601]),
544
- index=pd.DatetimeIndex([times, ]))
545
- assert_series_equal(expected, result)
559
+ if np.size(delta_t) == 1: # skip the array delta_t
560
+ times = datetime.datetime(2015, 1, 2)
561
+ result = solarposition.nrel_earthsun_distance(times, delta_t=delta_t)
562
+ expected = pd.Series(np.array([0.983289204601]),
563
+ index=pd.DatetimeIndex([times, ]))
564
+ assert_series_equal(expected, result)
546
565
 
547
566
 
548
567
  def test_equation_of_time():
@@ -579,19 +598,20 @@ def test_declination():
579
598
  def test_analytical_zenith():
580
599
  times = pd.date_range(start="1/1/2015 0:00", end="12/31/2015 23:00",
581
600
  freq="h").tz_localize('Etc/GMT+8')
601
+ times_utc = times.tz_convert('UTC')
582
602
  lat, lon = 37.8, -122.25
583
603
  lat_rad = np.deg2rad(lat)
584
604
  output = solarposition.spa_python(times, lat, lon, 100)
585
605
  solar_zenith = np.deg2rad(output['zenith']) # spa
586
606
  # spencer
587
- eot = solarposition.equation_of_time_spencer71(times.dayofyear)
607
+ eot = solarposition.equation_of_time_spencer71(times_utc.dayofyear)
588
608
  hour_angle = np.deg2rad(solarposition.hour_angle(times, lon, eot))
589
- decl = solarposition.declination_spencer71(times.dayofyear)
609
+ decl = solarposition.declination_spencer71(times_utc.dayofyear)
590
610
  zenith_1 = solarposition.solar_zenith_analytical(lat_rad, hour_angle, decl)
591
611
  # pvcdrom and cooper
592
- eot = solarposition.equation_of_time_pvcdrom(times.dayofyear)
612
+ eot = solarposition.equation_of_time_pvcdrom(times_utc.dayofyear)
593
613
  hour_angle = np.deg2rad(solarposition.hour_angle(times, lon, eot))
594
- decl = solarposition.declination_cooper69(times.dayofyear)
614
+ decl = solarposition.declination_cooper69(times_utc.dayofyear)
595
615
  zenith_2 = solarposition.solar_zenith_analytical(lat_rad, hour_angle, decl)
596
616
  assert np.allclose(zenith_1, solar_zenith, atol=0.015)
597
617
  assert np.allclose(zenith_2, solar_zenith, atol=0.025)
@@ -600,22 +620,23 @@ def test_analytical_zenith():
600
620
  def test_analytical_azimuth():
601
621
  times = pd.date_range(start="1/1/2015 0:00", end="12/31/2015 23:00",
602
622
  freq="h").tz_localize('Etc/GMT+8')
623
+ times_utc = times.tz_convert('UTC')
603
624
  lat, lon = 37.8, -122.25
604
625
  lat_rad = np.deg2rad(lat)
605
626
  output = solarposition.spa_python(times, lat, lon, 100)
606
627
  solar_azimuth = np.deg2rad(output['azimuth']) # spa
607
628
  solar_zenith = np.deg2rad(output['zenith'])
608
629
  # spencer
609
- eot = solarposition.equation_of_time_spencer71(times.dayofyear)
630
+ eot = solarposition.equation_of_time_spencer71(times_utc.dayofyear)
610
631
  hour_angle = np.deg2rad(solarposition.hour_angle(times, lon, eot))
611
- decl = solarposition.declination_spencer71(times.dayofyear)
632
+ decl = solarposition.declination_spencer71(times_utc.dayofyear)
612
633
  zenith = solarposition.solar_zenith_analytical(lat_rad, hour_angle, decl)
613
634
  azimuth_1 = solarposition.solar_azimuth_analytical(lat_rad, hour_angle,
614
635
  decl, zenith)
615
636
  # pvcdrom and cooper
616
- eot = solarposition.equation_of_time_pvcdrom(times.dayofyear)
637
+ eot = solarposition.equation_of_time_pvcdrom(times_utc.dayofyear)
617
638
  hour_angle = np.deg2rad(solarposition.hour_angle(times, lon, eot))
618
- decl = solarposition.declination_cooper69(times.dayofyear)
639
+ decl = solarposition.declination_cooper69(times_utc.dayofyear)
619
640
  zenith = solarposition.solar_zenith_analytical(lat_rad, hour_angle, decl)
620
641
  azimuth_2 = solarposition.solar_azimuth_analytical(lat_rad, hour_angle,
621
642
  decl, zenith)
@@ -665,21 +686,77 @@ def test_hour_angle():
665
686
  '2015-01-02 12:04:44.6340'
666
687
  ]).tz_localize('Etc/GMT+7')
667
688
  eot = np.array([-3.935172, -4.117227, -4.026295])
668
- hours = solarposition.hour_angle(times, longitude, eot)
689
+ hourangle = solarposition.hour_angle(times, longitude, eot)
669
690
  expected = (-70.682338, 70.72118825000001, 0.000801250)
670
691
  # FIXME: there are differences from expected NREL SPA calculator values
671
692
  # sunrise: 4 seconds, sunset: 48 seconds, transit: 0.2 seconds
672
693
  # but the differences may be due to other SPA input parameters
673
- assert np.allclose(hours, expected)
694
+ assert np.allclose(hourangle, expected)
695
+
696
+ hours = solarposition._hour_angle_to_hours(
697
+ times, hourangle, longitude, eot)
698
+ result = solarposition._times_to_hours_after_local_midnight(times)
699
+ assert np.allclose(result, hours)
700
+
701
+ result = solarposition._local_times_from_hours_since_midnight(times, hours)
702
+ assert result.equals(times)
703
+
704
+ times = times.tz_convert(None)
705
+ with pytest.raises(ValueError):
706
+ solarposition.hour_angle(times, longitude, eot)
707
+ with pytest.raises(ValueError):
708
+ solarposition._hour_angle_to_hours(times, hourangle, longitude, eot)
709
+ with pytest.raises(ValueError):
710
+ solarposition._times_to_hours_after_local_midnight(times)
711
+ with pytest.raises(ValueError):
712
+ solarposition._local_times_from_hours_since_midnight(times, hours)
713
+
714
+
715
+ def test_hour_angle_with_tricky_timezones():
716
+ # GH 2132
717
+ # tests timezones that have a DST shift at midnight
718
+
719
+ eot = np.array([-3.935172, -4.117227, -4.026295, -4.026295])
720
+
721
+ longitude = 70.6693
722
+ times = pd.DatetimeIndex([
723
+ '2014-09-06 23:00:00',
724
+ '2014-09-07 00:00:00',
725
+ '2014-09-07 01:00:00',
726
+ '2014-09-07 02:00:00',
727
+ ]).tz_localize('America/Santiago', nonexistent='shift_forward')
728
+
729
+ with pytest.raises(pytz.exceptions.NonExistentTimeError):
730
+ times.normalize()
731
+
732
+ # should not raise `pytz.exceptions.NonExistentTimeError`
733
+ solarposition.hour_angle(times, longitude, eot)
734
+
735
+ longitude = 82.3666
736
+ times = pd.DatetimeIndex([
737
+ '2014-11-01 23:00:00',
738
+ '2014-11-02 00:00:00',
739
+ '2014-11-02 01:00:00',
740
+ '2014-11-02 02:00:00',
741
+ ]).tz_localize('America/Havana', ambiguous=[True, True, False, False])
742
+
743
+ with pytest.raises(pytz.exceptions.AmbiguousTimeError):
744
+ solarposition.hour_angle(times, longitude, eot)
674
745
 
675
746
 
676
747
  def test_sun_rise_set_transit_geometric(expected_rise_set_spa, golden_mst):
677
748
  """Test geometric calculations for sunrise, sunset, and transit times"""
678
749
  times = expected_rise_set_spa.index
750
+ times_utc = times.tz_convert('UTC')
679
751
  latitude = golden_mst.latitude
680
752
  longitude = golden_mst.longitude
681
- eot = solarposition.equation_of_time_spencer71(times.dayofyear) # minutes
682
- decl = solarposition.declination_spencer71(times.dayofyear) # radians
753
+ eot = solarposition.equation_of_time_spencer71(
754
+ times_utc.dayofyear) # minutes
755
+ decl = solarposition.declination_spencer71(times_utc.dayofyear) # radians
756
+ with pytest.raises(ValueError):
757
+ solarposition.sun_rise_set_transit_geometric(
758
+ times.tz_convert(None), latitude=latitude, longitude=longitude,
759
+ declination=decl, equation_of_time=eot)
683
760
  sr, ss, st = solarposition.sun_rise_set_transit_geometric(
684
761
  times, latitude=latitude, longitude=longitude, declination=decl,
685
762
  equation_of_time=eot)
@@ -742,6 +819,7 @@ def test__datetime_to_unixtime_units(unit, tz):
742
819
 
743
820
 
744
821
  @requires_pandas_2_0
822
+ @pytest.mark.parametrize('tz', [None, 'utc', 'US/Eastern'])
745
823
  @pytest.mark.parametrize('method', [
746
824
  'nrel_numpy',
747
825
  'ephemeris',
@@ -749,7 +827,6 @@ def test__datetime_to_unixtime_units(unit, tz):
749
827
  pytest.param('nrel_numba', marks=requires_numba),
750
828
  pytest.param('nrel_c', marks=requires_spa_c),
751
829
  ])
752
- @pytest.mark.parametrize('tz', [None, 'utc', 'US/Eastern'])
753
830
  def test_get_solarposition_microsecond_index(method, tz):
754
831
  # https://github.com/pvlib/pvlib-python/issues/1932
755
832
 
@@ -758,8 +835,12 @@ def test_get_solarposition_microsecond_index(method, tz):
758
835
  index_ns = pd.date_range(unit='ns', **kwargs)
759
836
  index_us = pd.date_range(unit='us', **kwargs)
760
837
 
761
- sp_ns = solarposition.get_solarposition(index_ns, 40, -80, method=method)
762
- sp_us = solarposition.get_solarposition(index_us, 40, -80, method=method)
838
+ with warnings.catch_warnings():
839
+ # don't warn on method reload
840
+ warnings.simplefilter("ignore")
841
+
842
+ sp_ns = solarposition.get_solarposition(index_ns, 0, 0, method=method)
843
+ sp_us = solarposition.get_solarposition(index_us, 0, 0, method=method)
763
844
 
764
845
  assert_frame_equal(sp_ns, sp_us, check_index_type=False)
765
846
 
@@ -781,7 +862,7 @@ def test_nrel_earthsun_distance_microsecond_index(tz):
781
862
 
782
863
 
783
864
  @requires_pandas_2_0
784
- @pytest.mark.parametrize('tz', [None, 'utc', 'US/Eastern'])
865
+ @pytest.mark.parametrize('tz', ['utc', 'US/Eastern'])
785
866
  def test_hour_angle_microsecond_index(tz):
786
867
  # https://github.com/pvlib/pvlib-python/issues/1932
787
868
 
@@ -813,7 +894,7 @@ def test_rise_set_transit_spa_microsecond_index(tz):
813
894
 
814
895
 
815
896
  @requires_pandas_2_0
816
- @pytest.mark.parametrize('tz', [None, 'utc', 'US/Eastern'])
897
+ @pytest.mark.parametrize('tz', ['utc', 'US/Eastern'])
817
898
  def test_rise_set_transit_geometric_microsecond_index(tz):
818
899
  # https://github.com/pvlib/pvlib-python/issues/1932
819
900
 
@@ -838,7 +919,7 @@ def test_spa_python_numba_physical(expected_solpos, golden_mst):
838
919
  times = pd.date_range(datetime.datetime(2003, 10, 17, 12, 30, 30),
839
920
  periods=1, freq='D', tz=golden_mst.tz)
840
921
  with warnings.catch_warnings():
841
- # don't warn on method reload or num threads
922
+ # don't warn on method reload
842
923
  # ensure that numpy is the most recently used method so that
843
924
  # we can use the warns filter below
844
925
  warnings.simplefilter("ignore")
@@ -865,7 +946,7 @@ def test_spa_python_numba_physical_dst(expected_solpos, golden):
865
946
  periods=1, freq='D', tz=golden.tz)
866
947
 
867
948
  with warnings.catch_warnings():
868
- # don't warn on method reload or num threads
949
+ # don't warn on method reload
869
950
  warnings.simplefilter("ignore")
870
951
  ephem_data = solarposition.spa_python(times, golden.latitude,
871
952
  golden.longitude, pressure=82000,
pvlib/tests/test_spa.py CHANGED
@@ -1,6 +1,8 @@
1
1
  import os
2
2
  import datetime as dt
3
3
  import warnings
4
+ import pytest
5
+ import pvlib
4
6
 
5
7
  try:
6
8
  from importlib import reload
@@ -234,7 +236,7 @@ class SpaBase:
234
236
 
235
237
  def test_solar_position(self):
236
238
  with warnings.catch_warnings():
237
- # don't warn on method reload or num threads
239
+ # don't warn on method reload
238
240
  warnings.simplefilter("ignore")
239
241
  spa_out_0 = self.spa.solar_position(
240
242
  unixtimes, lat, lon, elev, pressure, temp, delta_t,
@@ -423,3 +425,30 @@ class NumbaSpaTest(unittest.TestCase, SpaBase):
423
425
  nresult, self.spa.solar_position(
424
426
  times, lat, lon, elev, pressure, temp, delta_t,
425
427
  atmos_refract, numthreads=3, sst=True)[:3], 5)
428
+
429
+
430
+ # Define extra test cases for issue #2077
431
+ test_cases_issue_2207 = [
432
+ ((2000, 1, 1, 12, 0, 0), 2451545.0),
433
+ ((1999, 1, 1, 0, 0, 0), 2451179.5),
434
+ ((1987, 1, 27, 0, 0, 0), 2446822.5),
435
+ ((1987, 6, 19, 12, 0, 0), 2446966.0),
436
+ ((1988, 1, 27, 0, 0, 0), 2447187.5),
437
+ ((1988, 6, 19, 12, 0, 0), 2447332.0),
438
+ ((1900, 1, 1, 0, 0, 0), 2415020.5),
439
+ ((1600, 1, 1, 0, 0, 0), 2305447.5),
440
+ ((1600, 12, 31, 0, 0, 0), 2305812.5),
441
+ ((837, 4, 10, 7, 12, 0), 2026871.8),
442
+ ((-123, 12, 31, 0, 0, 0), 1676496.5),
443
+ ((-122, 1, 1, 0, 0, 0), 1676497.5),
444
+ ((-1000, 7, 12, 12, 0, 0), 1356001.0),
445
+ ((-1000, 2, 29, 0, 0, 0), 1355866.5),
446
+ ((-1001, 8, 17, 21, 36, 0), 1355671.4),
447
+ ((-4712, 1, 1, 12, 0, 0), 0.0),
448
+ ]
449
+
450
+
451
+ @pytest.mark.parametrize("inputs, expected", test_cases_issue_2207)
452
+ def test_julian_day_issue_2207(inputs, expected):
453
+ result = pvlib.spa.julian_day_dt(*inputs, microsecond=0)
454
+ assert result == expected, f"Failed for inputs {inputs}"
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
- return pd_object.dayofyear
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 pd.DatetimeIndex([pd.Timestamp(time)]).dayofyear
257
+ return _pandas_to_doy(_datetimelike_scalar_to_datetimeindex(time))
234
258
 
235
259
 
236
260
  def _datetimelike_scalar_to_datetimeindex(time):