From 30bb3195b01fbe871d8075c22d02c7dbe8d14515 Mon Sep 17 00:00:00 2001 From: Scott Nelson Date: Mon, 15 Jul 2024 10:52:33 -0400 Subject: [PATCH 01/12] 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 | 22 ++++++++++++++++++++++ 3 files changed, 36 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..a2fdf88a1f 100644 --- a/pvlib/tests/test_solarposition.py +++ b/pvlib/tests/test_solarposition.py @@ -673,6 +673,28 @@ 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 From 6000bb26a2621892774c71aa13b51e7f76c52c3f Mon Sep 17 00:00:00 2001 From: Scott Nelson Date: Mon, 15 Jul 2024 17:00:51 -0400 Subject: [PATCH 02/12] Test that Pandas normalize raises time errors --- pvlib/tests/test_solarposition.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pvlib/tests/test_solarposition.py b/pvlib/tests/test_solarposition.py index a2fdf88a1f..cdde630c61 100644 --- a/pvlib/tests/test_solarposition.py +++ b/pvlib/tests/test_solarposition.py @@ -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 @@ -683,6 +684,10 @@ def test_hour_angle_with_tricky_timezones(): '2014-09-07 10:00:00', '2014-09-07 11:00:00', ]).tz_localize('America/Santiago') + + with pytest.raises(pytz.exceptions.NonExistentTimeError): + times.normalize() + # should not raise `pytz.exceptions.NonExistentTimeError` solarposition.hour_angle(times, longitude, eot) @@ -691,6 +696,10 @@ def test_hour_angle_with_tricky_timezones(): '2014-11-02 10:00:00', '2014-11-02 11:00:00', ]).tz_localize('America/Havana') + + with pytest.raises(pytz.exceptions.AmbiguousTimeError): + times.normalize() + # should not raise `pytz.exceptions.AmbiguousTimeError` solarposition.hour_angle(times, longitude, eot) From 6354443e36d76bc833af2cf0aad048f33d0d938d Mon Sep 17 00:00:00 2001 From: Scott Nelson Date: Mon, 15 Jul 2024 19:36:49 -0400 Subject: [PATCH 03/12] Fix whitespace --- pvlib/tests/test_solarposition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/tests/test_solarposition.py b/pvlib/tests/test_solarposition.py index cdde630c61..96cf2c3787 100644 --- a/pvlib/tests/test_solarposition.py +++ b/pvlib/tests/test_solarposition.py @@ -687,7 +687,7 @@ def test_hour_angle_with_tricky_timezones(): with pytest.raises(pytz.exceptions.NonExistentTimeError): times.normalize() - + # should not raise `pytz.exceptions.NonExistentTimeError` solarposition.hour_angle(times, longitude, eot) From 2348afd5387cb701465dd4a6b9570caabc8b8569 Mon Sep 17 00:00:00 2001 From: Scott Nelson Date: Wed, 17 Jul 2024 11:11:32 -0400 Subject: [PATCH 04/12] Fix handling of ambiguous times during DST shift --- pvlib/solarposition.py | 10 +++++++++- pvlib/tests/test_solarposition.py | 18 +++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index fa1bcc2340..8b577c6383 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -1392,6 +1392,14 @@ 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 + is_dst = [] + for time in times: + dst = time.tzinfo.dst(time) + if dst is None: + is_dst.append(False) + else: + is_dst.append(dst > dt.timedelta(seconds=0)) + # 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 @@ -1401,7 +1409,7 @@ def hour_angle(times, longitude, equation_of_time): # 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') + times.tz, nonexistent='shift_forward', ambiguous=is_dst) hrs_minus_tzs = (times - normalized_times) / pd.Timedelta('1h') - tzs diff --git a/pvlib/tests/test_solarposition.py b/pvlib/tests/test_solarposition.py index 96cf2c3787..332d242faa 100644 --- a/pvlib/tests/test_solarposition.py +++ b/pvlib/tests/test_solarposition.py @@ -677,13 +677,15 @@ def test_hour_angle(): def test_hour_angle_with_tricky_timezones(): # tests timezones that have a DST shift at midnight - eot = np.array([-3.935172, -4.117227]) + eot = np.array([-3.935172, -4.117227, -4.026295, -4.026295]) longitude = 70.6693 times = pd.DatetimeIndex([ - '2014-09-07 10:00:00', - '2014-09-07 11:00:00', - ]).tz_localize('America/Santiago') + '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() @@ -693,9 +695,11 @@ def test_hour_angle_with_tricky_timezones(): longitude = 82.3666 times = pd.DatetimeIndex([ - '2014-11-02 10:00:00', - '2014-11-02 11:00:00', - ]).tz_localize('America/Havana') + '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): times.normalize() From 844a8b04c10018a27413572f32d4c9009f63ed47 Mon Sep 17 00:00:00 2001 From: Scott Nelson Date: Wed, 17 Jul 2024 20:38:54 -0400 Subject: [PATCH 05/12] Raise excption on ambiguous time --- pvlib/solarposition.py | 10 +--------- pvlib/tests/test_solarposition.py | 5 +---- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index 8b577c6383..c0e00b95d7 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -1392,14 +1392,6 @@ 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 - is_dst = [] - for time in times: - dst = time.tzinfo.dst(time) - if dst is None: - is_dst.append(False) - else: - is_dst.append(dst > dt.timedelta(seconds=0)) - # 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 @@ -1409,7 +1401,7 @@ def hour_angle(times, longitude, equation_of_time): # 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=is_dst) + times.tz, nonexistent='shift_forward', ambiguous='raise') hrs_minus_tzs = (times - normalized_times) / pd.Timedelta('1h') - tzs diff --git a/pvlib/tests/test_solarposition.py b/pvlib/tests/test_solarposition.py index 332d242faa..7c4391d22a 100644 --- a/pvlib/tests/test_solarposition.py +++ b/pvlib/tests/test_solarposition.py @@ -702,10 +702,7 @@ def test_hour_angle_with_tricky_timezones(): ]).tz_localize('America/Havana', ambiguous=[True, True, False, False]) with pytest.raises(pytz.exceptions.AmbiguousTimeError): - times.normalize() - - # should not raise `pytz.exceptions.AmbiguousTimeError` - solarposition.hour_angle(times, longitude, eot) + solarposition.hour_angle(times, longitude, eot) def test_sun_rise_set_transit_geometric(expected_rise_set_spa, golden_mst): From 92b1d280797b14127d71eff43bf3db0f2a0e3482 Mon Sep 17 00:00:00 2001 From: Scott Nelson Date: Thu, 18 Jul 2024 12:28:38 -0400 Subject: [PATCH 06/12] Update pvlib/solarposition.py Co-authored-by: Cliff Hansen --- pvlib/solarposition.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index c0e00b95d7..cb516c9c04 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -1399,7 +1399,6 @@ def hour_angle(times, longitude, equation_of_time): 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') From 5b79e00786d4f7e8bb2063f067c074792caee1a9 Mon Sep 17 00:00:00 2001 From: Scott Nelson Date: Mon, 22 Jul 2024 12:52:13 -0400 Subject: [PATCH 07/12] Add scttnlsn to 0.11.1 contributors list --- docs/sphinx/source/whatsnew/v0.11.1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.11.1.rst b/docs/sphinx/source/whatsnew/v0.11.1.rst index 04ee918db7..e0554c0e9c 100644 --- a/docs/sphinx/source/whatsnew/v0.11.1.rst +++ b/docs/sphinx/source/whatsnew/v0.11.1.rst @@ -43,4 +43,4 @@ Contributors * Leonardo Micheli (:ghuser:`lmicheli`) * Echedey Luis (:ghuser:`echedey-ls`) * Rajiv Daxini (:ghuser:`RDaxini`) - +* Scott Nelson (:ghuser:`scttnlsn`) From 2466e7beceda789e4e0526497a57374c1f24ee99 Mon Sep 17 00:00:00 2001 From: Scott Nelson Date: Wed, 14 Aug 2024 06:12:23 -0400 Subject: [PATCH 08/12] Update pvlib/tests/test_solarposition.py Co-authored-by: Kevin Anderson --- pvlib/tests/test_solarposition.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pvlib/tests/test_solarposition.py b/pvlib/tests/test_solarposition.py index 7c4391d22a..4e9b38cdd6 100644 --- a/pvlib/tests/test_solarposition.py +++ b/pvlib/tests/test_solarposition.py @@ -675,6 +675,7 @@ def test_hour_angle(): def test_hour_angle_with_tricky_timezones(): + # GH 2132 # tests timezones that have a DST shift at midnight eot = np.array([-3.935172, -4.117227, -4.026295, -4.026295]) From 4ed2315749f3f27594f5267ad3f18b2b4ba7dee2 Mon Sep 17 00:00:00 2001 From: Scott Nelson Date: Wed, 14 Aug 2024 06:11:45 -0400 Subject: [PATCH 09/12] Update hour_angle docstring --- pvlib/solarposition.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index 12c26addd5..2bd579b5e9 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -1360,6 +1360,11 @@ def hour_angle(times, longitude, equation_of_time): times : :class:`pandas.DatetimeIndex` Corresponding timestamps, must be localized to the timezone for the ``longitude``. + + A `pytz.exceptions.AmbiguousTimeError` could be raised if any of the + given times are on a day where the local daylight savings transition happened + at midnight. If you're working with such a timezone, consider converting to + a non-DST timezone (i.e. GMT-4) before calling this function. longitude : numeric Longitude in degrees equation_of_time : numeric From 104bb2344bc78b9ed9d4f420c552c46383dd5303 Mon Sep 17 00:00:00 2001 From: Scott Nelson Date: Wed, 14 Aug 2024 08:13:22 -0400 Subject: [PATCH 10/12] Update pvlib/solarposition.py Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> --- pvlib/solarposition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index 2bd579b5e9..270765562d 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -1361,8 +1361,8 @@ def hour_angle(times, longitude, equation_of_time): Corresponding timestamps, must be localized to the timezone for the ``longitude``. - A `pytz.exceptions.AmbiguousTimeError` could be raised if any of the - given times are on a day where the local daylight savings transition happened + A `pytz.exceptions.AmbiguousTimeError` will be raised if any of the + given times are on a day when the local daylight savings transition happens at midnight. If you're working with such a timezone, consider converting to a non-DST timezone (i.e. GMT-4) before calling this function. longitude : numeric From 406924efed7c8aa9de821864e403fd4c0a4f7431 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 4 Dec 2024 10:57:11 -0500 Subject: [PATCH 11/12] move whatsnew entry to 0.11.2 --- docs/sphinx/source/whatsnew/v0.11.1.rst | 3 --- docs/sphinx/source/whatsnew/v0.11.2.rst | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.11.1.rst b/docs/sphinx/source/whatsnew/v0.11.1.rst index 7e7644034a..e1e4ef0d32 100644 --- a/docs/sphinx/source/whatsnew/v0.11.1.rst +++ b/docs/sphinx/source/whatsnew/v0.11.1.rst @@ -37,8 +37,6 @@ Enhancements Bug fixes ~~~~~~~~~ -* Handle DST transitions that happen at midnight in :py:func:`pvlib.solarposition.hour_angle` - (:issue:`2132` :pull:`2133`) * To prevent simulation output from differing slightly based on the time zone of the time stamps, models that use day of year for sun position and irradiance calculations now determine the day of year according to the UTC @@ -93,7 +91,6 @@ Contributors * Ioannis Sifnaios (:ghuser:`IoannisSifnaios`) * Leonardo Micheli (:ghuser:`lmicheli`) * Rajiv Daxini (:ghuser:`RDaxini`) -* Scott Nelson (:ghuser:`scttnlsn`) * Mark A. Mikofski (:ghuser:`mikofski`) * Ben Pierce (:ghuser:`bgpierc`) * Jose Meza (:ghuser:`JoseMezaMendieta`) diff --git a/docs/sphinx/source/whatsnew/v0.11.2.rst b/docs/sphinx/source/whatsnew/v0.11.2.rst index 49a1121c17..8fb16ed0a9 100644 --- a/docs/sphinx/source/whatsnew/v0.11.2.rst +++ b/docs/sphinx/source/whatsnew/v0.11.2.rst @@ -17,6 +17,8 @@ Bug Fixes ~~~~~~~~~ * :py:meth:`~pvlib.pvsystem.PVSystem.get_irradiance` accepts float inputs. (:issue:`1338`, :pull:`2227`) +* Handle DST transitions that happen at midnight in :py:func:`pvlib.solarposition.hour_angle` + (:issue:`2132` :pull:`2133`) Bug fixes ~~~~~~~~~ @@ -74,3 +76,4 @@ Contributors * matsuobasho (:ghuser:`matsuobasho`) * Echedey Luis (:ghuser:`echedey-ls`) * Kevin Anderson (:ghuser:`kandersolar`) +* Scott Nelson (:ghuser:`scttnlsn`) From 193595233488484c4876138d2b3a1ef559014376 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 4 Dec 2024 11:03:27 -0500 Subject: [PATCH 12/12] lint --- pvlib/solarposition.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index b88fe36cd2..b667ac04e0 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -1349,11 +1349,12 @@ def hour_angle(times, longitude, equation_of_time): times : :class:`pandas.DatetimeIndex` Corresponding timestamps, must be localized to the timezone for the ``longitude``. - + A `pytz.exceptions.AmbiguousTimeError` will be raised if any of the - given times are on a day when the local daylight savings transition happens - at midnight. If you're working with such a timezone, consider converting to - a non-DST timezone (i.e. GMT-4) before calling this function. + given times are on a day when the local daylight savings transition + happens at midnight. If you're working with such a timezone, + consider converting to a non-DST timezone (e.g. GMT-4) before + calling this function. longitude : numeric Longitude in degrees equation_of_time : numeric