From 9809fa0f9b82a9818e4eb40cacd22d3b7c14d8a0 Mon Sep 17 00:00:00 2001 From: Scott Nelson Date: Mon, 15 Jul 2024 10:52:33 -0400 Subject: [PATCH] Handle time normalization for nonexistent and ambiguous times --- docs/sphinx/source/whatsnew/v0.11.1.rst | 3 ++- pvlib/solarposition.py | 13 ++++++++++++- pvlib/tests/test_solarposition.py | 23 +++++++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.11.1.rst b/docs/sphinx/source/whatsnew/v0.11.1.rst index 5ffb0e564a..04ee918db7 100644 --- a/docs/sphinx/source/whatsnew/v0.11.1.rst +++ b/docs/sphinx/source/whatsnew/v0.11.1.rst @@ -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 ~~~~~~~ diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index af59184727..fa1bcc2340 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -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='infer') + + hrs_minus_tzs = (times - normalized_times) / pd.Timedelta('1h') - tzs # ensure array return instead of a version-dependent pandas Index return np.asarray( diff --git a/pvlib/tests/test_solarposition.py b/pvlib/tests/test_solarposition.py index 472383acce..ece41a60b4 100644 --- a/pvlib/tests/test_solarposition.py +++ b/pvlib/tests/test_solarposition.py @@ -673,6 +673,29 @@ 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]) + + longitude = 70.6693 + times = pd.DatetimeIndex([ + '2014-09-07 10:00:00', + '2014-09-07 11:00:00', + ]).tz_localize('America/Santiago') + # should not raise `pytz.exceptions.NonExistentTimeError` + solarposition.hour_angle(times, longitude, eot) + + longitude = 82.3666 + times = pd.DatetimeIndex([ + '2014-11-02 10:00:00', + '2014-11-02 11:00:00', + ]).tz_localize('America/Havana') + # should not raise `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