Skip to content

Allow asymmetrical rotation limits in pvlib.tracking.singleaxis (v2) #1852

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

Merged
merged 14 commits into from
Sep 13, 2023
Merged
6 changes: 6 additions & 0 deletions docs/sphinx/source/whatsnew/v0.10.2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ 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
:py:func:`pvlib.clearsky.detect_clearsky` (:issue:`1808`, :pull:`1784`)
* Added a continuous version of the Erbs diffuse-fraction/decomposition model.
Expand Down Expand Up @@ -56,6 +61,7 @@ Requirements
Contributors
~~~~~~~~~~~~
* Adam R. Jensen (:ghuser:`AdamRJensen`)
* Michal Arieli (:ghuser:`MichalArieli`)
* Abigail Jones (:ghuser:`ajonesr`)
* Taos Transue (:ghuser:`reepoi`)
* Echedey Luis (:ghuser:`echedey-ls`)
Expand Down
23 changes: 16 additions & 7 deletions pvlib/pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -1411,12 +1411,21 @@ 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 Expand Up @@ -1452,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
Expand Down
9 changes: 9 additions & 0 deletions pvlib/tests/test_pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
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,21 @@ 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 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 +199,16 @@ 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 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:
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