From 251deb78aeb00bc8569db9b2ba7fef8553b91d92 Mon Sep 17 00:00:00 2001 From: MichalArieli Date: Tue, 18 Jul 2023 17:06:32 +0300 Subject: [PATCH 01/11] Modify max_angle to accept tuples in addition to single values --- pvlib/pvsystem.py | 15 +++++++++----- pvlib/tests/test_tracking.py | 16 +++++++++++++++ pvlib/tracking.py | 38 +++++++++++++++++++++++++++--------- 3 files changed, 55 insertions(+), 14 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index bdab5d604d..ad512863f2 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1782,12 +1782,17 @@ class SingleAxisTrackerMount(AbstractMount): A value denoting the compass direction along which the axis of rotation lies, measured east of north. [degrees] - max_angle : float, default 90 - A value denoting the maximum rotation angle + max_angle : float or tuple, default 90 + A value denoting the maximum rotation angle, in decimal degrees, of the one-axis tracker from its horizontal position (horizontal - if axis_tilt = 0). A max_angle of 90 degrees allows the tracker - to rotate to a vertical position to point the panel towards a - horizon. max_angle of 180 degrees allows for full rotation. [degrees] + if axis_tilt = 0). If a float is provided, it represents the maximum + rotation angle, and the minimum rotation angle is assumed to be the + opposite of the maximum angle. If a tuple of (min_angle, max_angle) + is provided, it represents both the minimum and maximum rotation angles. + + A max_angle of 90 degrees allows the tracker to rotate to a vertical + position to point the panel towards a horizon. A max_angle of 180 degrees + allows for full rotation. backtrack : bool, default True Controls whether the tracker has the capability to "backtrack" diff --git a/pvlib/tests/test_tracking.py b/pvlib/tests/test_tracking.py index 87452939f5..fd2008aca4 100644 --- a/pvlib/tests/test_tracking.py +++ b/pvlib/tests/test_tracking.py @@ -151,6 +151,22 @@ def test_max_angle(): assert_frame_equal(expect, tracker_data) +def test_min_angle(): + apparent_zenith = pd.Series([60]) + apparent_azimuth = pd.Series([270]) + tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth, + axis_tilt=0, axis_azimuth=0, + max_angle=(-45, 50), backtrack=True, + gcr=2.0/7.0) + + expect = pd.DataFrame({'aoi': 15, 'surface_azimuth': 270, + 'surface_tilt': 45, 'tracker_theta': -45}, + index=[0], dtype=np.float64) + expect = expect[SINGLEAXIS_COL_ORDER] + + assert_frame_equal(expect, tracker_data) + + def test_backtrack(): apparent_zenith = pd.Series([80]) apparent_azimuth = pd.Series([90]) diff --git a/pvlib/tracking.py b/pvlib/tracking.py index 9ae148447f..1a6a71ed15 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -27,12 +27,17 @@ class SingleAxisTracker(PVSystem): A value denoting the compass direction along which the axis of rotation lies. Measured in decimal degrees east of north. - max_angle : float, default 90 + max_angle : float or tuple, default 90 A value denoting the maximum rotation angle, in decimal degrees, of the one-axis tracker from its horizontal position (horizontal - if axis_tilt = 0). A max_angle of 90 degrees allows the tracker - to rotate to a vertical position to point the panel towards a - horizon. max_angle of 180 degrees allows for full rotation. + if axis_tilt = 0). If a float is provided, it represents the maximum + rotation angle, and the minimum rotation angle is assumed to be the + opposite of the maximum angle. If a tuple of (min_angle, max_angle) + is provided, it represents both the minimum and maximum rotation angles. + + A max_angle of 90 degrees allows the tracker to rotate to a vertical + position to point the panel towards a horizon. A max_angle of 180 degrees + allows for full rotation. backtrack : bool, default True Controls whether the tracker has the capability to "backtrack" @@ -312,12 +317,17 @@ def singleaxis(apparent_zenith, apparent_azimuth, A value denoting the compass direction along which the axis of rotation lies. Measured in decimal degrees east of north. - max_angle : float, default 90 + max_angle : float or tuple, default 90 A value denoting the maximum rotation angle, in decimal degrees, of the one-axis tracker from its horizontal position (horizontal - if axis_tilt = 0). A max_angle of 90 degrees allows the tracker - to rotate to a vertical position to point the panel towards a - horizon. max_angle of 180 degrees allows for full rotation. + if axis_tilt = 0). If a float is provided, it represents the maximum + rotation angle, and the minimum rotation angle is assumed to be the + opposite of the maximum angle. If a tuple of (min_angle, max_angle) + is provided, it represents both the minimum and maximum rotation angles. + + A max_angle of 90 degrees allows the tracker to rotate to a vertical + position to point the panel towards a horizon. A max_angle of 180 degrees + allows for full rotation. backtrack : bool, default True Controls whether the tracker has the capability to "backtrack" @@ -458,7 +468,17 @@ def singleaxis(apparent_zenith, apparent_azimuth, # NOTE: max_angle defined relative to zero-point rotation, not the # system-plane normal - tracker_theta = np.clip(tracker_theta, -max_angle, max_angle) + + + # Determine minimum and maximum rotation angles for the tracker based on max_angle. + # If max_angle is a single value, assume min_angle is the negative of max_angle. + if np.array(max_angle).size == 1: + min_angle = -max_angle + else: + min_angle, max_angle = max_angle + + # Clip tracker_theta between the minimum and maximum angles. + tracker_theta = np.clip(tracker_theta, min_angle, max_angle) # Calculate auxiliary angles surface = calc_surface_orientation(tracker_theta, axis_tilt, axis_azimuth) From ebb424029aa795ea6d79c8df737e3438033a6b82 Mon Sep 17 00:00:00 2001 From: MichalArieli Date: Sun, 23 Jul 2023 13:11:11 +0300 Subject: [PATCH 02/11] add text --- pvlib/pvsystem.py | 9 ++++++--- pvlib/tracking.py | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index e06f996472..08693ecec7 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1418,9 +1418,12 @@ class SingleAxisTrackerMount(AbstractMount): opposite of the maximum angle. If a tuple of (min_angle, max_angle) is provided, it represents both the minimum and maximum rotation angles. - A max_angle of 90 degrees allows the tracker to rotate to a vertical - position to point the panel towards a horizon. A max_angle of 180 degrees - allows for full rotation. + A rotation to 'max_angle' is a counter-clockwise rotation about the y-axis + of the tracker coordinate system. For example, for a tracker with 'axis_azimuth' + oriented to the south, a rotation to 'max_angle' is towards the west, and a + rotation toward 'min_angle' is in the opposite direction, toward the east. + Hence a max_angle of 180 degrees (equivalent to max_angle = (-180, 180)) allows + the tracker to achieve its full rotation capability. backtrack : bool, default True Controls whether the tracker has the capability to "backtrack" diff --git a/pvlib/tracking.py b/pvlib/tracking.py index 5a6b7d494c..8f06583afa 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -52,9 +52,12 @@ def singleaxis(apparent_zenith, apparent_azimuth, opposite of the maximum angle. If a tuple of (min_angle, max_angle) is provided, it represents both the minimum and maximum rotation angles. - A max_angle of 90 degrees allows the tracker to rotate to a vertical - position to point the panel towards a horizon. A max_angle of 180 degrees - allows for full rotation. + A rotation to 'max_angle' is a counter-clockwise rotation about the y-axis + of the tracker coordinate system. For example, for a tracker with 'axis_azimuth' + oriented to the south, a rotation to 'max_angle' is towards the west, and a + rotation toward 'min_angle' is in the opposite direction, toward the east. + Hence a max_angle of 180 degrees (equivalent to max_angle = (-180, 180)) allows + the tracker to achieve its full rotation capability. backtrack : bool, default True Controls whether the tracker has the capability to "backtrack" From 0a9a760848d30f31ebdbdd4ab05445a61c2412e1 Mon Sep 17 00:00:00 2001 From: MichalArieli Date: Sun, 23 Jul 2023 13:21:05 +0300 Subject: [PATCH 03/11] add comment to docs/sphinx/source/whatsnew/v0.10.2 --- docs/sphinx/source/whatsnew/v0.10.2.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.10.2.rst b/docs/sphinx/source/whatsnew/v0.10.2.rst index f95f0bd93d..0a218c8f0a 100644 --- a/docs/sphinx/source/whatsnew/v0.10.2.rst +++ b/docs/sphinx/source/whatsnew/v0.10.2.rst @@ -15,7 +15,9 @@ Enhancements :py:func:`pvlib.iotools.get_pvgis_hourly`, :py:func:`pvlib.iotools.get_cams`, :py:func:`pvlib.iotools.get_bsrn`, and :py:func:`pvlib.iotools.read_midc_raw_data_from_nrel`. (:pull:`1800`) - +* Added support for asymmetric limiting angles in :py:func:`pvlib.tracking.singleaxis`, by modifying + the `min_angle` parameter to accept tuples. + (:pull:`1809`) Bug fixes ~~~~~~~~~ @@ -36,3 +38,4 @@ Requirements Contributors ~~~~~~~~~~~~ * Adam R. Jensen (:ghuser:`AdamRJensen`) +* Michal Arieli (:ghuser:`MichalArieli`) From 90bf22d1ab62e8ea608c35299b1da5272891039b Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 12 Sep 2023 12:21:04 -0400 Subject: [PATCH 04/11] reflow docstrings for line length limit --- pvlib/pvsystem.py | 17 +++++++++-------- pvlib/tracking.py | 17 +++++++++-------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 4b853465c5..e908d4efac 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1416,15 +1416,16 @@ class SingleAxisTrackerMount(AbstractMount): of the one-axis tracker from its horizontal position (horizontal if axis_tilt = 0). If a float is provided, it represents the maximum rotation angle, and the minimum rotation angle is assumed to be the - opposite of the maximum angle. If a tuple of (min_angle, max_angle) - is provided, it represents both the minimum and maximum rotation angles. + opposite of the maximum angle. If a tuple of (min_angle, max_angle) is + provided, it represents both the minimum and maximum rotation angles. - A rotation to 'max_angle' is a counter-clockwise rotation about the y-axis - of the tracker coordinate system. For example, for a tracker with 'axis_azimuth' - oriented to the south, a rotation to 'max_angle' is towards the west, and a - rotation toward 'min_angle' is in the opposite direction, toward the east. - Hence a max_angle of 180 degrees (equivalent to max_angle = (-180, 180)) allows - the tracker to achieve its full rotation capability. + A rotation to 'max_angle' is a counter-clockwise rotation about the + y-axis of the tracker coordinate system. For example, for a tracker + with 'axis_azimuth' oriented to the south, a rotation to 'max_angle' + is towards the west, and a rotation toward 'min_angle' is in the + opposite direction, toward the east. Hence a max_angle of 180 degrees + (equivalent to max_angle = (-180, 180)) allows the tracker to achieve + its full rotation capability. backtrack : bool, default True Controls whether the tracker has the capability to "backtrack" diff --git a/pvlib/tracking.py b/pvlib/tracking.py index 8f06583afa..4570c9d3a5 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -49,15 +49,16 @@ def singleaxis(apparent_zenith, apparent_azimuth, of the one-axis tracker from its horizontal position (horizontal if axis_tilt = 0). If a float is provided, it represents the maximum rotation angle, and the minimum rotation angle is assumed to be the - opposite of the maximum angle. If a tuple of (min_angle, max_angle) - is provided, it represents both the minimum and maximum rotation angles. + opposite of the maximum angle. If a tuple of (min_angle, max_angle) is + provided, it represents both the minimum and maximum rotation angles. - A rotation to 'max_angle' is a counter-clockwise rotation about the y-axis - of the tracker coordinate system. For example, for a tracker with 'axis_azimuth' - oriented to the south, a rotation to 'max_angle' is towards the west, and a - rotation toward 'min_angle' is in the opposite direction, toward the east. - Hence a max_angle of 180 degrees (equivalent to max_angle = (-180, 180)) allows - the tracker to achieve its full rotation capability. + A rotation to 'max_angle' is a counter-clockwise rotation about the + y-axis of the tracker coordinate system. For example, for a tracker + with 'axis_azimuth' oriented to the south, a rotation to 'max_angle' + is towards the west, and a rotation toward 'min_angle' is in the + opposite direction, toward the east. Hence a max_angle of 180 degrees + (equivalent to max_angle = (-180, 180)) allows the tracker to achieve + its full rotation capability. backtrack : bool, default True Controls whether the tracker has the capability to "backtrack" From 61be55c51520cd05cc2aa636231d4389560ce140 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 12 Sep 2023 12:21:11 -0400 Subject: [PATCH 05/11] address PR suggestions --- docs/sphinx/source/whatsnew/v0.10.2.rst | 3 +++ pvlib/tracking.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.10.2.rst b/docs/sphinx/source/whatsnew/v0.10.2.rst index 70168c920e..839c150ee2 100644 --- a/docs/sphinx/source/whatsnew/v0.10.2.rst +++ b/docs/sphinx/source/whatsnew/v0.10.2.rst @@ -18,6 +18,9 @@ Enhancements * Added support for asymmetric limiting angles in :py:func:`pvlib.tracking.singleaxis`, by modifying the `min_angle` parameter to accept tuples. (:pull:`1809`) +* Added support for asymmetric limiting angles in :py:func:`pvlib.tracking.singleaxis` + and :py:class:`pvlib.pvsystem.SingleAxisTrackerMount` by modifying the ``max_angle`` + parameter to accept tuples. (:issue:`1777`, :pull:`1809`) * Added option to infer threshold values for :py:func:`pvlib.clearsky.detect_clearsky` (:issue:`1808`, :pull:`1784`) * Added a continuous version of the Erbs diffuse-fraction/decomposition model. diff --git a/pvlib/tracking.py b/pvlib/tracking.py index 4570c9d3a5..db53ab57cc 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -203,7 +203,7 @@ def singleaxis(apparent_zenith, apparent_azimuth, # Determine minimum and maximum rotation angles for the tracker based on max_angle. # If max_angle is a single value, assume min_angle is the negative of max_angle. - if np.array(max_angle).size == 1: + if np.isscalar(max_angle): min_angle = -max_angle else: min_angle, max_angle = max_angle From 437c110994a3b9b1619c6edeb0491b5bc108e63c Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 12 Sep 2023 12:30:43 -0400 Subject: [PATCH 06/11] update type hint in SingleAxisTrackerMount --- pvlib/pvsystem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index e908d4efac..eeb415c852 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -15,7 +15,7 @@ import pandas as pd from dataclasses import dataclass from abc import ABC, abstractmethod -from typing import Optional +from typing import Optional, Union from pvlib._deprecation import deprecated, warn_deprecated @@ -1461,7 +1461,7 @@ class SingleAxisTrackerMount(AbstractMount): """ axis_tilt: float = 0.0 axis_azimuth: float = 0.0 - max_angle: float = 90.0 + max_angle: Union[float, tuple] = 90.0 backtrack: bool = True gcr: float = 2.0/7.0 cross_axis_tilt: float = 0.0 From 6d4080416dc8934e613f1f3a0846aeef4084c70d Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 12 Sep 2023 12:31:35 -0400 Subject: [PATCH 07/11] simplify whatsnew --- docs/sphinx/source/whatsnew/v0.10.2.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.10.2.rst b/docs/sphinx/source/whatsnew/v0.10.2.rst index 839c150ee2..e0c2212222 100644 --- a/docs/sphinx/source/whatsnew/v0.10.2.rst +++ b/docs/sphinx/source/whatsnew/v0.10.2.rst @@ -19,8 +19,7 @@ Enhancements the `min_angle` parameter to accept tuples. (:pull:`1809`) * Added support for asymmetric limiting angles in :py:func:`pvlib.tracking.singleaxis` - and :py:class:`pvlib.pvsystem.SingleAxisTrackerMount` by modifying the ``max_angle`` - parameter to accept tuples. (:issue:`1777`, :pull:`1809`) + and :py:class:`~pvlib.pvsystem.SingleAxisTrackerMount. (:issue:`1777`, :pull:`1809`) * Added option to infer threshold values for :py:func:`pvlib.clearsky.detect_clearsky` (:issue:`1808`, :pull:`1784`) * Added a continuous version of the Erbs diffuse-fraction/decomposition model. From dd3e4e7e03bed0323404e8ce75d6c270c643a070 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 12 Sep 2023 12:35:01 -0400 Subject: [PATCH 08/11] PR number --- docs/sphinx/source/whatsnew/v0.10.2.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.10.2.rst b/docs/sphinx/source/whatsnew/v0.10.2.rst index e0c2212222..0130b58eb6 100644 --- a/docs/sphinx/source/whatsnew/v0.10.2.rst +++ b/docs/sphinx/source/whatsnew/v0.10.2.rst @@ -19,7 +19,7 @@ Enhancements the `min_angle` parameter to accept tuples. (:pull:`1809`) * Added support for asymmetric limiting angles in :py:func:`pvlib.tracking.singleaxis` - and :py:class:`~pvlib.pvsystem.SingleAxisTrackerMount. (:issue:`1777`, :pull:`1809`) + and :py:class:`~pvlib.pvsystem.SingleAxisTrackerMount. (:issue:`1777`, :pull:`1809`, :pull:`1852`) * Added option to infer threshold values for :py:func:`pvlib.clearsky.detect_clearsky` (:issue:`1808`, :pull:`1784`) * Added a continuous version of the Erbs diffuse-fraction/decomposition model. From a51059f1f5939b82fb9010f2e167d6e2e5d5b30b Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 12 Sep 2023 12:44:25 -0400 Subject: [PATCH 09/11] add SingleAxisTrackerMount test --- pvlib/tests/test_pvsystem.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index b379dd41bc..862d082775 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -2422,6 +2422,15 @@ def test_SingleAxisTrackerMount_get_orientation(single_axis_tracker_mount): assert actual[key] == pytest.approx(expected_value), err_msg +def test_SingleAxisTrackerMount_get_orientation_asymmetric_max(): + mount = pvsystem.SingleAxisTrackerMount(max_angle=(-30, 45)) + expected = {'surface_tilt': [45, 30], 'surface_azimuth': [90, 270]} + actual = mount.get_orientation([60, 60], [90, 270]) + for key, expected_value in expected.items(): + err_msg = f"{key} value incorrect" + assert actual[key] == pytest.approx(expected_value), err_msg + + def test_dc_ohms_from_percent(): expected = .1425 out = pvsystem.dc_ohms_from_percent(38, 8, 3, 1, 1) From 1be64e652123d49e1aedaf6ee171846884b18a2d Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 12 Sep 2023 12:51:35 -0400 Subject: [PATCH 10/11] lint --- pvlib/pvsystem.py | 2 +- pvlib/tracking.py | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index eeb415c852..2743340d04 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1418,7 +1418,7 @@ class SingleAxisTrackerMount(AbstractMount): rotation angle, and the minimum rotation angle is assumed to be the opposite of the maximum angle. If a tuple of (min_angle, max_angle) is provided, it represents both the minimum and maximum rotation angles. - + A rotation to 'max_angle' is a counter-clockwise rotation about the y-axis of the tracker coordinate system. For example, for a tracker with 'axis_azimuth' oriented to the south, a rotation to 'max_angle' diff --git a/pvlib/tracking.py b/pvlib/tracking.py index db53ab57cc..04ed5f8506 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -51,7 +51,7 @@ def singleaxis(apparent_zenith, apparent_azimuth, rotation angle, and the minimum rotation angle is assumed to be the opposite of the maximum angle. If a tuple of (min_angle, max_angle) is provided, it represents both the minimum and maximum rotation angles. - + A rotation to 'max_angle' is a counter-clockwise rotation about the y-axis of the tracker coordinate system. For example, for a tracker with 'axis_azimuth' oriented to the south, a rotation to 'max_angle' @@ -200,12 +200,11 @@ def singleaxis(apparent_zenith, apparent_azimuth, # NOTE: max_angle defined relative to zero-point rotation, not the # system-plane normal - - # Determine minimum and maximum rotation angles for the tracker based on max_angle. - # If max_angle is a single value, assume min_angle is the negative of max_angle. + # Determine minimum and maximum rotation angles based on max_angle. + # If max_angle is a single value, assume min_angle is the negative. if np.isscalar(max_angle): min_angle = -max_angle - else: + else: min_angle, max_angle = max_angle # Clip tracker_theta between the minimum and maximum angles. From 1d21499c956a48692ddbc2a7fdcd5c179e535a16 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 13 Sep 2023 15:58:07 -0400 Subject: [PATCH 11/11] remove duplicate whatsnew entry --- docs/sphinx/source/whatsnew/v0.10.2.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.10.2.rst b/docs/sphinx/source/whatsnew/v0.10.2.rst index 0130b58eb6..9a0a4f9145 100644 --- a/docs/sphinx/source/whatsnew/v0.10.2.rst +++ b/docs/sphinx/source/whatsnew/v0.10.2.rst @@ -15,9 +15,6 @@ Enhancements :py:func:`pvlib.iotools.get_pvgis_hourly`, :py:func:`pvlib.iotools.get_cams`, :py:func:`pvlib.iotools.get_bsrn`, and :py:func:`pvlib.iotools.read_midc_raw_data_from_nrel`. (:pull:`1800`) -* Added support for asymmetric limiting angles in :py:func:`pvlib.tracking.singleaxis`, by modifying - the `min_angle` parameter to accept tuples. - (:pull:`1809`) * Added support for asymmetric limiting angles in :py:func:`pvlib.tracking.singleaxis` and :py:class:`~pvlib.pvsystem.SingleAxisTrackerMount. (:issue:`1777`, :pull:`1809`, :pull:`1852`) * Added option to infer threshold values for