Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/sphinx/source/whatsnew/v0.11.1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ Enhancements

Bug fixes
~~~~~~~~~

* Handle DST transitions that happen at midnight in :py:func:`pvlib.solarposition.hour_angle`
(:issue:`2132` :pull:`2133`)

Testing
~~~~~~~
Expand Down
13 changes: 12 additions & 1 deletion pvlib/solarposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -1392,7 +1392,18 @@ def hour_angle(times, longitude, equation_of_time):
times = times.tz_localize('utc')
tzs = np.array([ts.utcoffset().total_seconds() for ts in times]) / 3600

hrs_minus_tzs = (times - times.normalize()) / pd.Timedelta('1h') - tzs
# Some timezones have a DST shift at midnight:
# 11:59pm -> 1:00am - results in a nonexistent midnight
# 12:59am -> 12:00am - results in an ambiguous midnight
# We remove the timezone before normalizing for this reason.
naive_normalized_times = times.tz_localize(None).normalize()

# Use Pandas functionality for shifting nonexistent times forward
# or infering ambiguous times (which arose from normalizing)
normalized_times = naive_normalized_times.tz_localize(
times.tz, nonexistent='shift_forward', ambiguous='raise')

hrs_minus_tzs = (times - normalized_times) / pd.Timedelta('1h') - tzs

# ensure array return instead of a version-dependent pandas <T>Index
return np.asarray(
Expand Down
32 changes: 32 additions & 0 deletions pvlib/tests/test_solarposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .conftest import assert_frame_equal, assert_series_equal
from numpy.testing import assert_allclose
import pytest
import pytz

from pvlib.location import Location
from pvlib import solarposition, spa
Expand Down Expand Up @@ -673,6 +674,37 @@ def test_hour_angle():
assert np.allclose(hours, expected)


def test_hour_angle_with_tricky_timezones():
# tests timezones that have a DST shift at midnight

eot = np.array([-3.935172, -4.117227, -4.026295, -4.026295])

longitude = 70.6693
times = pd.DatetimeIndex([
'2014-09-06 23:00:00',
'2014-09-07 00:00:00',
'2014-09-07 01:00:00',
'2014-09-07 02:00:00',
]).tz_localize('America/Santiago', nonexistent='shift_forward')

with pytest.raises(pytz.exceptions.NonExistentTimeError):
times.normalize()

# should not raise `pytz.exceptions.NonExistentTimeError`
solarposition.hour_angle(times, longitude, eot)

longitude = 82.3666
times = pd.DatetimeIndex([
'2014-11-01 23:00:00',
'2014-11-02 00:00:00',
'2014-11-02 01:00:00',
'2014-11-02 02:00:00',
]).tz_localize('America/Havana', ambiguous=[True, True, False, False])

with pytest.raises(pytz.exceptions.AmbiguousTimeError):
solarposition.hour_angle(times, longitude, eot)


def test_sun_rise_set_transit_geometric(expected_rise_set_spa, golden_mst):
"""Test geometric calculations for sunrise, sunset, and transit times"""
times = expected_rise_set_spa.index
Expand Down