Skip to content

Allow asymmetrical rotation limits in pvlib.tracking.singleaxis #1809

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all 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
5 changes: 4 additions & 1 deletion docs/sphinx/source/whatsnew/v0.10.2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Comment on lines +18 to +20
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* 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`)


Bug fixes
~~~~~~~~~
Expand All @@ -36,3 +38,4 @@ Requirements
Contributors
~~~~~~~~~~~~
* Adam R. Jensen (:ghuser:`AdamRJensen`)
* Michal Arieli (:ghuser:`MichalArieli`)
18 changes: 13 additions & 5 deletions pvlib/pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -1410,12 +1410,20 @@ 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 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"
Expand Down
16 changes: 16 additions & 0 deletions pvlib/tests/test_tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
28 changes: 23 additions & 5 deletions pvlib/tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,20 @@ 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.

Copy link
Member

@cwhanse cwhanse Jul 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we need to explain the orientation that distinguishes min_angle from max_angle. Suggest adding a new paragraph like:

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this input! As you suggested, I have added this paragraph.

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"
Expand Down Expand Up @@ -190,7 +198,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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if np.array(max_angle).size == 1:
if np.isscalar(max_angle):

Maybe a bit easier to read while achieving the same thing?

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)
Expand Down