Skip to content

Commit 4237a67

Browse files
Allow asymmetrical rotation limits in pvlib.tracking.singleaxis (v2) (#1852)
* Modify max_angle to accept tuples in addition to single values * add text * add comment to docs/sphinx/source/whatsnew/v0.10.2 * reflow docstrings for line length limit * address PR suggestions * update type hint in SingleAxisTrackerMount * simplify whatsnew * PR number * add SingleAxisTrackerMount test * lint * remove duplicate whatsnew entry --------- Co-authored-by: MichalArieli <[email protected]> Co-authored-by: MichalArieli <[email protected]>
1 parent 5f7109b commit 4237a67

File tree

5 files changed

+67
-12
lines changed

5 files changed

+67
-12
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Enhancements
1515
:py:func:`pvlib.iotools.get_pvgis_hourly`, :py:func:`pvlib.iotools.get_cams`,
1616
:py:func:`pvlib.iotools.get_bsrn`, and :py:func:`pvlib.iotools.read_midc_raw_data_from_nrel`.
1717
(:pull:`1800`)
18+
* Added support for asymmetric limiting angles in :py:func:`pvlib.tracking.singleaxis`
19+
and :py:class:`~pvlib.pvsystem.SingleAxisTrackerMount. (:issue:`1777`, :pull:`1809`, :pull:`1852`)
1820
* Added option to infer threshold values for
1921
:py:func:`pvlib.clearsky.detect_clearsky` (:issue:`1808`, :pull:`1784`)
2022
* Added a continuous version of the Erbs diffuse-fraction/decomposition model.
@@ -67,6 +69,7 @@ Requirements
6769
Contributors
6870
~~~~~~~~~~~~
6971
* Adam R. Jensen (:ghuser:`AdamRJensen`)
72+
* Michal Arieli (:ghuser:`MichalArieli`)
7073
* Abigail Jones (:ghuser:`ajonesr`)
7174
* Taos Transue (:ghuser:`reepoi`)
7275
* Echedey Luis (:ghuser:`echedey-ls`)

pvlib/pvsystem.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import pandas as pd
1616
from dataclasses import dataclass
1717
from abc import ABC, abstractmethod
18-
from typing import Optional
18+
from typing import Optional, Union
1919

2020
from pvlib._deprecation import deprecated, warn_deprecated
2121

@@ -1412,12 +1412,21 @@ class SingleAxisTrackerMount(AbstractMount):
14121412
A value denoting the compass direction along which the axis of
14131413
rotation lies, measured east of north. [degrees]
14141414
1415-
max_angle : float, default 90
1416-
A value denoting the maximum rotation angle
1415+
max_angle : float or tuple, default 90
1416+
A value denoting the maximum rotation angle, in decimal degrees,
14171417
of the one-axis tracker from its horizontal position (horizontal
1418-
if axis_tilt = 0). A max_angle of 90 degrees allows the tracker
1419-
to rotate to a vertical position to point the panel towards a
1420-
horizon. max_angle of 180 degrees allows for full rotation. [degrees]
1418+
if axis_tilt = 0). If a float is provided, it represents the maximum
1419+
rotation angle, and the minimum rotation angle is assumed to be the
1420+
opposite of the maximum angle. If a tuple of (min_angle, max_angle) is
1421+
provided, it represents both the minimum and maximum rotation angles.
1422+
1423+
A rotation to 'max_angle' is a counter-clockwise rotation about the
1424+
y-axis of the tracker coordinate system. For example, for a tracker
1425+
with 'axis_azimuth' oriented to the south, a rotation to 'max_angle'
1426+
is towards the west, and a rotation toward 'min_angle' is in the
1427+
opposite direction, toward the east. Hence a max_angle of 180 degrees
1428+
(equivalent to max_angle = (-180, 180)) allows the tracker to achieve
1429+
its full rotation capability.
14211430
14221431
backtrack : bool, default True
14231432
Controls whether the tracker has the capability to "backtrack"
@@ -1453,7 +1462,7 @@ class SingleAxisTrackerMount(AbstractMount):
14531462
"""
14541463
axis_tilt: float = 0.0
14551464
axis_azimuth: float = 0.0
1456-
max_angle: float = 90.0
1465+
max_angle: Union[float, tuple] = 90.0
14571466
backtrack: bool = True
14581467
gcr: float = 2.0/7.0
14591468
cross_axis_tilt: float = 0.0

pvlib/tests/test_pvsystem.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2428,6 +2428,15 @@ def test_SingleAxisTrackerMount_get_orientation(single_axis_tracker_mount):
24282428
assert actual[key] == pytest.approx(expected_value), err_msg
24292429

24302430

2431+
def test_SingleAxisTrackerMount_get_orientation_asymmetric_max():
2432+
mount = pvsystem.SingleAxisTrackerMount(max_angle=(-30, 45))
2433+
expected = {'surface_tilt': [45, 30], 'surface_azimuth': [90, 270]}
2434+
actual = mount.get_orientation([60, 60], [90, 270])
2435+
for key, expected_value in expected.items():
2436+
err_msg = f"{key} value incorrect"
2437+
assert actual[key] == pytest.approx(expected_value), err_msg
2438+
2439+
24312440
def test_dc_ohms_from_percent():
24322441
expected = .1425
24332442
out = pvsystem.dc_ohms_from_percent(38, 8, 3, 1, 1)

pvlib/tests/test_tracking.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,22 @@ def test_max_angle():
151151
assert_frame_equal(expect, tracker_data)
152152

153153

154+
def test_min_angle():
155+
apparent_zenith = pd.Series([60])
156+
apparent_azimuth = pd.Series([270])
157+
tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth,
158+
axis_tilt=0, axis_azimuth=0,
159+
max_angle=(-45, 50), backtrack=True,
160+
gcr=2.0/7.0)
161+
162+
expect = pd.DataFrame({'aoi': 15, 'surface_azimuth': 270,
163+
'surface_tilt': 45, 'tracker_theta': -45},
164+
index=[0], dtype=np.float64)
165+
expect = expect[SINGLEAXIS_COL_ORDER]
166+
167+
assert_frame_equal(expect, tracker_data)
168+
169+
154170
def test_backtrack():
155171
apparent_zenith = pd.Series([80])
156172
apparent_azimuth = pd.Series([90])

pvlib/tracking.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,21 @@ def singleaxis(apparent_zenith, apparent_azimuth,
4444
A value denoting the compass direction along which the axis of
4545
rotation lies. Measured in decimal degrees east of north.
4646
47-
max_angle : float, default 90
47+
max_angle : float or tuple, default 90
4848
A value denoting the maximum rotation angle, in decimal degrees,
4949
of the one-axis tracker from its horizontal position (horizontal
50-
if axis_tilt = 0). A max_angle of 90 degrees allows the tracker
51-
to rotate to a vertical position to point the panel towards a
52-
horizon. max_angle of 180 degrees allows for full rotation.
50+
if axis_tilt = 0). If a float is provided, it represents the maximum
51+
rotation angle, and the minimum rotation angle is assumed to be the
52+
opposite of the maximum angle. If a tuple of (min_angle, max_angle) is
53+
provided, it represents both the minimum and maximum rotation angles.
54+
55+
A rotation to 'max_angle' is a counter-clockwise rotation about the
56+
y-axis of the tracker coordinate system. For example, for a tracker
57+
with 'axis_azimuth' oriented to the south, a rotation to 'max_angle'
58+
is towards the west, and a rotation toward 'min_angle' is in the
59+
opposite direction, toward the east. Hence a max_angle of 180 degrees
60+
(equivalent to max_angle = (-180, 180)) allows the tracker to achieve
61+
its full rotation capability.
5362
5463
backtrack : bool, default True
5564
Controls whether the tracker has the capability to "backtrack"
@@ -190,7 +199,16 @@ def singleaxis(apparent_zenith, apparent_azimuth,
190199

191200
# NOTE: max_angle defined relative to zero-point rotation, not the
192201
# system-plane normal
193-
tracker_theta = np.clip(tracker_theta, -max_angle, max_angle)
202+
203+
# Determine minimum and maximum rotation angles based on max_angle.
204+
# If max_angle is a single value, assume min_angle is the negative.
205+
if np.isscalar(max_angle):
206+
min_angle = -max_angle
207+
else:
208+
min_angle, max_angle = max_angle
209+
210+
# Clip tracker_theta between the minimum and maximum angles.
211+
tracker_theta = np.clip(tracker_theta, min_angle, max_angle)
194212

195213
# Calculate auxiliary angles
196214
surface = calc_surface_orientation(tracker_theta, axis_tilt, axis_azimuth)

0 commit comments

Comments
 (0)