Skip to content

Commit 6af80da

Browse files
scttnlsncwhansekandersolarAdamRJensen
authored
Handle time normalization for nonexistent and ambiguous times (#2133)
* Handle time normalization for nonexistent and ambiguous times * Test that Pandas normalize raises time errors * Fix whitespace * Fix handling of ambiguous times during DST shift * Raise excption on ambiguous time * Update pvlib/solarposition.py Co-authored-by: Cliff Hansen <[email protected]> * Add scttnlsn to 0.11.1 contributors list * Update pvlib/tests/test_solarposition.py Co-authored-by: Kevin Anderson <[email protected]> * Update hour_angle docstring * Update pvlib/solarposition.py Co-authored-by: Adam R. Jensen <[email protected]> * move whatsnew entry to 0.11.2 * lint --------- Co-authored-by: Cliff Hansen <[email protected]> Co-authored-by: Kevin Anderson <[email protected]> Co-authored-by: Adam R. Jensen <[email protected]>
1 parent 6d886dc commit 6af80da

File tree

3 files changed

+53
-1
lines changed

3 files changed

+53
-1
lines changed

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

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ Bug Fixes
1717
~~~~~~~~~
1818
* :py:meth:`~pvlib.pvsystem.PVSystem.get_irradiance` accepts float inputs.
1919
(:issue:`1338`, :pull:`2227`)
20+
* Handle DST transitions that happen at midnight in :py:func:`pvlib.solarposition.hour_angle`
21+
(:issue:`2132` :pull:`2133`)
2022

2123
Bug fixes
2224
~~~~~~~~~
@@ -74,3 +76,4 @@ Contributors
7476
* matsuobasho (:ghuser:`matsuobasho`)
7577
* Echedey Luis (:ghuser:`echedey-ls`)
7678
* Kevin Anderson (:ghuser:`kandersolar`)
79+
* Scott Nelson (:ghuser:`scttnlsn`)

pvlib/solarposition.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -1349,6 +1349,12 @@ def hour_angle(times, longitude, equation_of_time):
13491349
times : :class:`pandas.DatetimeIndex`
13501350
Corresponding timestamps, must be localized to the timezone for the
13511351
``longitude``.
1352+
1353+
A `pytz.exceptions.AmbiguousTimeError` will be raised if any of the
1354+
given times are on a day when the local daylight savings transition
1355+
happens at midnight. If you're working with such a timezone,
1356+
consider converting to a non-DST timezone (e.g. GMT-4) before
1357+
calling this function.
13521358
longitude : numeric
13531359
Longitude in degrees
13541360
equation_of_time : numeric
@@ -1421,7 +1427,17 @@ def _times_to_hours_after_local_midnight(times):
14211427
if not times.tz:
14221428
raise ValueError('times must be localized')
14231429

1424-
hrs = (times - times.normalize()) / pd.Timedelta('1h')
1430+
# Some timezones have a DST shift at midnight:
1431+
# 11:59pm -> 1:00am - results in a nonexistent midnight
1432+
# 12:59am -> 12:00am - results in an ambiguous midnight
1433+
# We remove the timezone before normalizing for this reason.
1434+
naive_normalized_times = times.tz_localize(None).normalize()
1435+
1436+
# Use Pandas functionality for shifting nonexistent times forward
1437+
normalized_times = naive_normalized_times.tz_localize(
1438+
times.tz, nonexistent='shift_forward', ambiguous='raise')
1439+
1440+
hrs = (times - normalized_times) / pd.Timedelta('1h')
14251441

14261442
# ensure array return instead of a version-dependent pandas <T>Index
14271443
return np.array(hrs)

pvlib/tests/test_solarposition.py

+33
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .conftest import assert_frame_equal, assert_series_equal
99
from numpy.testing import assert_allclose
1010
import pytest
11+
import pytz
1112

1213
from pvlib.location import Location
1314
from pvlib import solarposition, spa
@@ -711,6 +712,38 @@ def test_hour_angle():
711712
solarposition._local_times_from_hours_since_midnight(times, hours)
712713

713714

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)
745+
746+
714747
def test_sun_rise_set_transit_geometric(expected_rise_set_spa, golden_mst):
715748
"""Test geometric calculations for sunrise, sunset, and transit times"""
716749
times = expected_rise_set_spa.index

0 commit comments

Comments
 (0)