Skip to content

Commit 5803ce3

Browse files
Allow delta_t to be array in SPA functions (#2190)
* code changes: allow delta_t to be array * initial test updates * whatsnew * lint * fix dtype issue * more delta_t array tests * optimize numba/numpy reloads and warnings * more lint * Update pvlib/spa.py Co-authored-by: Adam R. Jensen <[email protected]> * add test with varying delta_t --------- Co-authored-by: Adam R. Jensen <[email protected]>
1 parent 94c3fc5 commit 5803ce3

File tree

4 files changed

+70
-48
lines changed

4 files changed

+70
-48
lines changed

docs/sphinx/source/whatsnew/v0.11.1.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ Enhancements
3030
* Added function for calculating wind speed at different heights,
3131
:py:func:`pvlib.atmosphere.windspeed_powerlaw`.
3232
(:issue:`2118`, :pull:`2124`)
33+
* ``delta_t`` can now be specified with an array in the SPA functions.
34+
The numba implementation now also allows calculation of ``delta_t``
35+
internally. (:issue:`2189`, :pull:`2190`)
3336
* The multithreaded SPA functions no longer emit a warning when calculating
3437
solar positions for short time series. (:pull:`2170`)
3538
* Implemented closed-form solution for alpha in :py:func:`pvlib.clearsky.detect_clearsky`,
@@ -98,6 +101,7 @@ Contributors
98101
* Mark A. Mikofski (:ghuser:`mikofski`)
99102
* Ben Pierce (:ghuser:`bgpierc`)
100103
* Jose Meza (:ghuser:`JoseMezaMendieta`)
104+
* Kevin Anderson (:ghuser:`kandersolar`)
101105
* Luiz Reis (:ghuser:`luizreiscver`)
102106
* Carlos Cárdenas-Bravo (:ghuser:`cardenca`)
103107
* Marcos R. Escudero (:ghuser:`marc-resc`)

pvlib/solarposition.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -308,13 +308,11 @@ def spa_python(time, latitude, longitude,
308308
avg. yearly air pressure in Pascals.
309309
temperature : int or float, optional, default 12
310310
avg. yearly air temperature in degrees C.
311-
delta_t : float, optional, default 67.0
311+
delta_t : float or array, optional, default 67.0
312312
Difference between terrestrial time and UT1.
313313
If delta_t is None, uses spa.calculate_deltat
314314
using time.year and time.month from pandas.DatetimeIndex.
315315
For most simulations the default delta_t is sufficient.
316-
*Note: delta_t = None will break code using nrel_numba,
317-
this will be fixed in a future version.*
318316
The USNO has historical and forecasted delta_t [3]_.
319317
atmos_refrac : float, optional
320318
The approximate atmospheric refraction (in degrees)
@@ -374,7 +372,7 @@ def spa_python(time, latitude, longitude,
374372

375373
spa = _spa_python_import(how)
376374

377-
if not delta_t:
375+
if delta_t is None:
378376
time_utc = tools._pandas_to_utc(time)
379377
delta_t = spa.calculate_deltat(time_utc.year, time_utc.month)
380378

@@ -416,13 +414,11 @@ def sun_rise_set_transit_spa(times, latitude, longitude, how='numpy',
416414
Options are 'numpy' or 'numba'. If numba >= 0.17.0
417415
is installed, how='numba' will compile the spa functions
418416
to machine code and run them multithreaded.
419-
delta_t : float, optional, default 67.0
417+
delta_t : float or array, optional, default 67.0
420418
Difference between terrestrial time and UT1.
421419
If delta_t is None, uses spa.calculate_deltat
422420
using times.year and times.month from pandas.DatetimeIndex.
423421
For most simulations the default delta_t is sufficient.
424-
*Note: delta_t = None will break code using nrel_numba,
425-
this will be fixed in a future version.*
426422
numthreads : int, optional, default 4
427423
Number of threads to use if how == 'numba'.
428424
@@ -455,7 +451,7 @@ def sun_rise_set_transit_spa(times, latitude, longitude, how='numpy',
455451

456452
spa = _spa_python_import(how)
457453

458-
if not delta_t:
454+
if delta_t is None:
459455
delta_t = spa.calculate_deltat(times_utc.year, times_utc.month)
460456

461457
transit, sunrise, sunset = spa.transit_sunrise_sunset(
@@ -973,13 +969,11 @@ def nrel_earthsun_distance(time, how='numpy', delta_t=67.0, numthreads=4):
973969
is installed, how='numba' will compile the spa functions
974970
to machine code and run them multithreaded.
975971
976-
delta_t : float, optional, default 67.0
972+
delta_t : float or array, optional, default 67.0
977973
Difference between terrestrial time and UT1.
978974
If delta_t is None, uses spa.calculate_deltat
979975
using time.year and time.month from pandas.DatetimeIndex.
980976
For most simulations the default delta_t is sufficient.
981-
*Note: delta_t = None will break code using nrel_numba,
982-
this will be fixed in a future version.*
983977
984978
numthreads : int, optional, default 4
985979
Number of threads to use if how == 'numba'.
@@ -1006,7 +1000,7 @@ def nrel_earthsun_distance(time, how='numpy', delta_t=67.0, numthreads=4):
10061000

10071001
spa = _spa_python_import(how)
10081002

1009-
if not delta_t:
1003+
if delta_t is None:
10101004
time_utc = tools._pandas_to_utc(time)
10111005
delta_t = spa.calculate_deltat(time_utc.year, time_utc.month)
10121006

pvlib/spa.py

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -835,24 +835,24 @@ def equation_of_time(sun_mean_longitude, geocentric_sun_right_ascension,
835835
return E
836836

837837

838-
@jcompile('void(float64[:], float64[:], float64[:,:])', nopython=True,
839-
nogil=True)
840-
def solar_position_loop(unixtime, loc_args, out):
838+
@jcompile('void(float64[:], float64[:], float64[:], float64[:,:])',
839+
nopython=True, nogil=True)
840+
def solar_position_loop(unixtime, delta_t, loc_args, out):
841841
"""Loop through the time array and calculate the solar position"""
842842
lat = loc_args[0]
843843
lon = loc_args[1]
844844
elev = loc_args[2]
845845
pressure = loc_args[3]
846846
temp = loc_args[4]
847-
delta_t = loc_args[5]
848-
atmos_refract = loc_args[6]
849-
sst = loc_args[7]
850-
esd = loc_args[8]
847+
atmos_refract = loc_args[5]
848+
sst = loc_args[6]
849+
esd = loc_args[7]
851850

852851
for i in range(unixtime.shape[0]):
853852
utime = unixtime[i]
853+
dT = delta_t[i]
854854
jd = julian_day(utime)
855-
jde = julian_ephemeris_day(jd, delta_t)
855+
jde = julian_ephemeris_day(jd, dT)
856856
jc = julian_century(jd)
857857
jce = julian_ephemeris_century(jde)
858858
jme = julian_ephemeris_millennium(jce)
@@ -920,8 +920,11 @@ def solar_position_numba(unixtime, lat, lon, elev, pressure, temp, delta_t,
920920
and multiple threads. Very slow if functions are not numba compiled.
921921
"""
922922
# these args are the same for each thread
923-
loc_args = np.array([lat, lon, elev, pressure, temp, delta_t,
924-
atmos_refract, sst, esd])
923+
loc_args = np.array([lat, lon, elev, pressure, temp,
924+
atmos_refract, sst, esd], dtype=np.float64)
925+
926+
# turn delta_t into an array if it isn't already
927+
delta_t = np.full_like(unixtime, delta_t, dtype=np.float64)
925928

926929
# construct dims x ulength array to put the results in
927930
ulength = unixtime.shape[0]
@@ -940,13 +943,17 @@ def solar_position_numba(unixtime, lat, lon, elev, pressure, temp, delta_t,
940943
numthreads = ulength
941944

942945
if numthreads <= 1:
943-
solar_position_loop(unixtime, loc_args, result)
946+
solar_position_loop(unixtime, delta_t, loc_args, result)
944947
return result
945948

946949
# split the input and output arrays into numthreads chunks
947950
split0 = np.array_split(unixtime, numthreads)
951+
split1 = np.array_split(delta_t, numthreads)
948952
split2 = np.array_split(result, numthreads, axis=1)
949-
chunks = [[a0, loc_args, split2[i]] for i, a0 in enumerate(split0)]
953+
chunks = [
954+
[a0, a1, loc_args, a2]
955+
for a0, a1, a2 in zip(split0, split1, split2)
956+
]
950957
# Spawn one thread per chunk
951958
threads = [threading.Thread(target=solar_position_loop, args=chunk)
952959
for chunk in chunks]
@@ -1033,7 +1040,7 @@ def solar_position(unixtime, lat, lon, elev, pressure, temp, delta_t,
10331040
unixtime : numpy array
10341041
Array of unix/epoch timestamps to calculate solar position for.
10351042
Unixtime is the number of seconds since Jan. 1, 1970 00:00:00 UTC.
1036-
A pandas.DatetimeIndex is easily converted using .astype(np.int64)/10**9
1043+
A pandas.DatetimeIndex is easily converted using .view(np.int64)/10**9
10371044
lat : float
10381045
Latitude to calculate solar position for
10391046
lon : float
@@ -1046,7 +1053,7 @@ def solar_position(unixtime, lat, lon, elev, pressure, temp, delta_t,
10461053
temp : int or float
10471054
avg. yearly temperature at location in
10481055
degrees C; used for atmospheric correction
1049-
delta_t : float
1056+
delta_t : float or array
10501057
Difference between terrestrial time and UT1.
10511058
atmos_refrac : float
10521059
The approximate atmospheric refraction (in degrees)
@@ -1111,7 +1118,7 @@ def transit_sunrise_sunset(dates, lat, lon, delta_t, numthreads):
11111118
Latitude of location to perform calculation for
11121119
lon : float
11131120
Longitude of location
1114-
delta_t : float
1121+
delta_t : float or array
11151122
Difference between terrestrial time and UT. USNO has tables.
11161123
numthreads : int
11171124
Number to threads to use for calculation (if using numba)
@@ -1212,8 +1219,8 @@ def earthsun_distance(unixtime, delta_t, numthreads):
12121219
unixtime : numpy array
12131220
Array of unix/epoch timestamps to calculate solar position for.
12141221
Unixtime is the number of seconds since Jan. 1, 1970 00:00:00 UTC.
1215-
A pandas.DatetimeIndex is easily converted using .astype(np.int64)/10**9
1216-
delta_t : float
1222+
A pandas.DatetimeIndex is easily converted using .view(np.int64)/10**9
1223+
delta_t : float or array
12171224
Difference between terrestrial time and UT. USNO has tables.
12181225
numthreads : int
12191226
Number to threads to use for calculation (if using numba)
@@ -1240,9 +1247,6 @@ def calculate_deltat(year, month):
12401247
"""Calculate the difference between Terrestrial Dynamical Time (TD)
12411248
and Universal Time (UT).
12421249
1243-
Note: This function is not yet compatible for calculations using
1244-
Numba.
1245-
12461250
Equations taken from http://eclipse.gsfc.nasa.gov/SEcat5/deltatpoly.html
12471251
"""
12481252

pvlib/tests/test_solarposition.py

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def test_spa_python_numpy_physical_dst(expected_solpos, golden):
139139
assert_frame_equal(expected_solpos, ephem_data[expected_solpos.columns])
140140

141141

142-
@pytest.mark.parametrize('delta_t', [65.0, None])
142+
@pytest.mark.parametrize('delta_t', [65.0, None, np.array([65, 65])])
143143
def test_sun_rise_set_transit_spa(expected_rise_set_spa, golden, delta_t):
144144
# solution from NREL SAP web calculator
145145
south = Location(-35.0, 0.0, tz='UTC')
@@ -478,14 +478,14 @@ def test_get_solarposition_altitude(
478478

479479

480480
@pytest.mark.parametrize("delta_t, method", [
481-
(None, 'nrel_numpy'),
482-
pytest.param(
483-
None, 'nrel_numba',
484-
marks=[pytest.mark.xfail(
485-
reason='spa.calculate_deltat not implemented for numba yet')]),
481+
(None, 'nrel_numba'),
486482
(67.0, 'nrel_numba'),
483+
(np.array([67.0, 67.0]), 'nrel_numba'),
484+
# minimize reloads, with numpy being last
485+
(None, 'nrel_numpy'),
487486
(67.0, 'nrel_numpy'),
488-
])
487+
(np.array([67.0, 67.0]), 'nrel_numpy'),
488+
])
489489
def test_get_solarposition_deltat(delta_t, method, expected_solpos_multi,
490490
golden):
491491
times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30),
@@ -506,6 +506,21 @@ def test_get_solarposition_deltat(delta_t, method, expected_solpos_multi,
506506
assert_frame_equal(this_expected, ephem_data[this_expected.columns])
507507

508508

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+
509524
def test_get_solarposition_no_kwargs(expected_solpos, golden):
510525
times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30),
511526
periods=1, freq='D', tz=golden.tz)
@@ -530,7 +545,7 @@ def test_get_solarposition_method_pyephem(expected_solpos, golden):
530545
assert_frame_equal(expected_solpos, ephem_data[expected_solpos.columns])
531546

532547

533-
@pytest.mark.parametrize('delta_t', [64.0, None])
548+
@pytest.mark.parametrize('delta_t', [64.0, None, np.array([64, 64])])
534549
def test_nrel_earthsun_distance(delta_t):
535550
times = pd.DatetimeIndex([datetime.datetime(2015, 1, 2),
536551
datetime.datetime(2015, 8, 2)]
@@ -540,11 +555,12 @@ def test_nrel_earthsun_distance(delta_t):
540555
index=times)
541556
assert_series_equal(expected, result)
542557

543-
times = datetime.datetime(2015, 1, 2)
544-
result = solarposition.nrel_earthsun_distance(times, delta_t=delta_t)
545-
expected = pd.Series(np.array([0.983289204601]),
546-
index=pd.DatetimeIndex([times, ]))
547-
assert_series_equal(expected, result)
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)
548564

549565

550566
def test_equation_of_time():
@@ -770,14 +786,14 @@ def test__datetime_to_unixtime_units(unit, tz):
770786

771787

772788
@requires_pandas_2_0
789+
@pytest.mark.parametrize('tz', [None, 'utc', 'US/Eastern'])
773790
@pytest.mark.parametrize('method', [
774791
'nrel_numpy',
775792
'ephemeris',
776793
pytest.param('pyephem', marks=requires_ephem),
777794
pytest.param('nrel_numba', marks=requires_numba),
778795
pytest.param('nrel_c', marks=requires_spa_c),
779796
])
780-
@pytest.mark.parametrize('tz', [None, 'utc', 'US/Eastern'])
781797
def test_get_solarposition_microsecond_index(method, tz):
782798
# https://github.com/pvlib/pvlib-python/issues/1932
783799

@@ -786,8 +802,12 @@ def test_get_solarposition_microsecond_index(method, tz):
786802
index_ns = pd.date_range(unit='ns', **kwargs)
787803
index_us = pd.date_range(unit='us', **kwargs)
788804

789-
sp_ns = solarposition.get_solarposition(index_ns, 40, -80, method=method)
790-
sp_us = solarposition.get_solarposition(index_us, 40, -80, method=method)
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)
791811

792812
assert_frame_equal(sp_ns, sp_us, check_index_type=False)
793813

0 commit comments

Comments
 (0)