From e5fa03cc604413abd964e091fefc58ac14d63274 Mon Sep 17 00:00:00 2001 From: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Date: Fri, 8 Mar 2024 16:36:47 +0100 Subject: [PATCH] Projected zenith convenience function (#1904) * Function prototype * Update shading.rst * Update shading.py * Minimal test * Implementation From NREL paper * Fix, fix, fix, fix & format * Format issues * Extend tests (compare with singleaxis) & format with ruff * Format fixes * Upgrade tests * Array -> Axis * type * Whatsnew * xd * bruh * Minor Python optimization a la tracking.singleaxis * Comment and minor optimizations * Typo found by Mikofski Reported at: https://github.com/pvlib/pvlib-python/pull/1725#discussion_r1190610810 Confirmed via "Slope-Aware Backtracking for Single-Axis Trackers", paragraph after Eq. 1 Co-Authored-By: Mark Mikofski * Surface -> Axis Co-Authored-By: Kevin Anderson <57452607+kandersolar@users.noreply.github.com> * Elevation -> Zenith Co-Authored-By: Kevin Anderson <57452607+kandersolar@users.noreply.github.com> * Elev -> Zenith Co-Authored-By: Kevin Anderson <57452607+kandersolar@users.noreply.github.com> * Update shading.py * Update docstring Co-Authored-By: Anton Driesse <9001027+adriesse@users.noreply.github.com> * Add comments from `tracking.singleaxis` Co-Authored-By: Will Holmgren Co-Authored-By: Mark Mikofski * Singleaxis implementation port & test addition, based on old pvlib.tracking.singleaxis * Update v0.10.4.rst * Linter * Code review Co-Authored-By: Cliff Hansen <5393711+cwhanse@users.noreply.github.com> * Add Fig 5 [1] (still gotta check the built output) * Add caption, change size and describe in alternate text * rST fixes ? * Figures have captions, images do not https://pandemic-overview.readthedocs.io/en/latest/myGuides/reStructuredText-Images-and-Figures-Examples.html#id18 * Flip arguments order * I forgot :skull: * Linter are you happy now? * Remove port test and add edge cases test Co-Authored-By: Kevin Anderson <57452607+kandersolar@users.noreply.github.com> * Update test_shading.py Co-Authored-By: Kevin Anderson <57452607+kandersolar@users.noreply.github.com> * Indentation xd * Update test_shading.py * I forgot how to code * Align data * Docstring suggestion from Kevin Co-Authored-By: Kevin Anderson <57452607+kandersolar@users.noreply.github.com> * Update link to example? * Link, please work * Update shading.py * Update shading.py * Update shading.py * Update shading.py * Update shading.py * Update shading.py * Update shading.py * Update shading.py * Lintaaaaaaarrrgh Fixed the link finally * Update pvlib/shading.py Co-authored-by: Kevin Anderson --------- Co-authored-by: Mark Mikofski Co-authored-by: Kevin Anderson <57452607+kandersolar@users.noreply.github.com> Co-authored-by: Anton Driesse <9001027+adriesse@users.noreply.github.com> Co-authored-by: Will Holmgren Co-authored-by: Cliff Hansen <5393711+cwhanse@users.noreply.github.com> Co-authored-by: Kevin Anderson --- .../_images/Anderson_Mikofski_2020_Fig5.jpg | Bin 0 -> 35024 bytes .../effects_on_pv_system_output/shading.rst | 3 +- docs/sphinx/source/whatsnew/v0.10.4.rst | 3 + pvlib/shading.py | 110 ++++++++++++++ pvlib/tests/test_shading.py | 141 ++++++++++++++++-- pvlib/tracking.py | 46 +----- 6 files changed, 253 insertions(+), 50 deletions(-) create mode 100644 docs/sphinx/source/_images/Anderson_Mikofski_2020_Fig5.jpg diff --git a/docs/sphinx/source/_images/Anderson_Mikofski_2020_Fig5.jpg b/docs/sphinx/source/_images/Anderson_Mikofski_2020_Fig5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1bd2ecc9960657d16e2c70608293b775a84a6dfa GIT binary patch literal 35024 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 literal 0 HcmV?d00001 diff --git a/docs/sphinx/source/reference/effects_on_pv_system_output/shading.rst b/docs/sphinx/source/reference/effects_on_pv_system_output/shading.rst index 14ac13b4ca..a0fd74a795 100644 --- a/docs/sphinx/source/reference/effects_on_pv_system_output/shading.rst +++ b/docs/sphinx/source/reference/effects_on_pv_system_output/shading.rst @@ -9,4 +9,5 @@ Shading shading.ground_angle shading.masking_angle shading.masking_angle_passias - shading.sky_diffuse_passias \ No newline at end of file + shading.sky_diffuse_passias + shading.projected_solar_zenith_angle diff --git a/docs/sphinx/source/whatsnew/v0.10.4.rst b/docs/sphinx/source/whatsnew/v0.10.4.rst index c1c977b713..30abfbfe2a 100644 --- a/docs/sphinx/source/whatsnew/v0.10.4.rst +++ b/docs/sphinx/source/whatsnew/v0.10.4.rst @@ -8,11 +8,14 @@ v0.10.4 (Anticipated March, 2024) Enhancements ~~~~~~~~~~~~ * Added the Huld PV model used by PVGIS (:pull:`1940`) +* Added function :py:func:`pvlib.shading.projected_solar_zenith_angle`, + a common calculation in shading and tracking. (:issue:`1734`, :pull:`1904`) * Added :py:func:`~pvlib.iotools.get_solrad` for fetching irradiance data from the SOLRAD ground station network. (:pull:`1967`) * Added metadata parsing to :py:func:`~pvlib.iotools.read_solrad` to follow the standard iotools convention of returning a tuple of (data, meta). Previously the function only returned a dataframe. (:pull:`1968`) + Bug fixes ~~~~~~~~~ * Fixed an error in solar position calculations when using diff --git a/pvlib/shading.py b/pvlib/shading.py index c0a7a91f18..007e24e1b7 100644 --- a/pvlib/shading.py +++ b/pvlib/shading.py @@ -232,3 +232,113 @@ def sky_diffuse_passias(masking_angle): Available at https://www.nrel.gov/docs/fy18osti/67399.pdf """ return 1 - cosd(masking_angle/2)**2 + + +def projected_solar_zenith_angle(solar_zenith, solar_azimuth, + axis_tilt, axis_azimuth): + r""" + Calculate projected solar zenith angle in degrees. + + This solar zenith angle is projected onto the plane whose normal vector is + defined by ``axis_tilt`` and ``axis_azimuth``. The normal vector is in the + direction of ``axis_azimuth`` (clockwise from north) and tilted from + horizontal by ``axis_tilt``. See Figure 5 in [1]_: + + .. figure:: ../../_images/Anderson_Mikofski_2020_Fig5.jpg + :alt: Wire diagram of coordinates systems to obtain the projected angle. + :align: center + :scale: 50 % + + Fig. 5, [1]_: Solar coordinates projection onto tracker rotation plane. + + Parameters + ---------- + solar_zenith : numeric + Sun's apparent zenith in degrees. + solar_azimuth : numeric + Sun's azimuth in degrees. + axis_tilt : numeric + Axis tilt angle in degrees. From horizontal plane to array plane. + axis_azimuth : numeric + Axis azimuth angle in degrees. + North = 0°; East = 90°; South = 180°; West = 270° + + Returns + ------- + Projected_solar_zenith : numeric + In degrees. + + Notes + ----- + This projection has a variety of applications in PV. For example: + + - Projecting the sun's position onto the plane perpendicular to + the axis of a single-axis tracker (i.e. the plane + whose normal vector coincides with the tracker torque tube) + yields the tracker rotation angle that maximizes direct irradiance + capture. This tracking strategy is called *true-tracking*. Learn more + about tracking in + :ref:`sphx_glr_gallery_solar-tracking_plot_single_axis_tracking.py`. + + - Self-shading in large PV arrays is often modeled by assuming + a simplified 2-D array geometry where the sun's position is + projected onto the plane perpendicular to the PV rows. + The projected zenith angle is then used for calculations + regarding row-to-row shading. + + Examples + -------- + Calculate the ideal true-tracking angle for a horizontal north-south + single-axis tracker: + + >>> rotation = projected_solar_zenith_angle(solar_zenith, solar_azimuth, + >>> axis_tilt=0, axis_azimuth=180) + + Calculate the projected zenith angle in a south-facing fixed tilt array + (note: the ``axis_azimuth`` of a fixed-tilt row points along the length + of the row): + + >>> psza = projected_solar_zenith_angle(solar_zenith, solar_azimuth, + >>> axis_tilt=0, axis_azimuth=90) + + References + ---------- + .. [1] K. Anderson and M. Mikofski, 'Slope-Aware Backtracking for + Single-Axis Trackers', National Renewable Energy Lab. (NREL), Golden, + CO (United States); + NREL/TP-5K00-76626, Jul. 2020. :doi:`10.2172/1660126`. + + See Also + -------- + pvlib.solarposition.get_solarposition + """ + # Assume the tracker reference frame is right-handed. Positive y-axis is + # oriented along tracking axis; from north, the y-axis is rotated clockwise + # by the axis azimuth and tilted from horizontal by the axis tilt. The + # positive x-axis is 90 deg clockwise from the y-axis and parallel to + # horizontal (e.g., if the y-axis is south, the x-axis is west); the + # positive z-axis is normal to the x and y axes, pointed upward. + + # Since elevation = 90 - zenith, sin(90-x) = cos(x) & cos(90-x) = sin(x): + # Notation from [1], modified to use zenith instead of elevation + # cos(elevation) = sin(zenith) and sin(elevation) = cos(zenith) + # Avoid recalculating these values + sind_solar_zenith = sind(solar_zenith) + cosd_axis_azimuth = cosd(axis_azimuth) + sind_axis_azimuth = sind(axis_azimuth) + sind_axis_tilt = sind(axis_tilt) + + # Sun's x, y, z coords + sx = sind_solar_zenith * sind(solar_azimuth) + sy = sind_solar_zenith * cosd(solar_azimuth) + sz = cosd(solar_zenith) + # Eq. (4); sx', sz' values from sun coordinates projected onto surface + sx_prime = sx * cosd_axis_azimuth - sy * sind_axis_azimuth + sz_prime = ( + sx * sind_axis_azimuth * sind_axis_tilt + + sy * sind_axis_tilt * cosd_axis_azimuth + + sz * cosd(axis_tilt) + ) + # Eq. (5); angle between sun's beam and surface + theta_T = np.degrees(np.arctan2(sx_prime, sz_prime)) + return theta_T diff --git a/pvlib/tests/test_shading.py b/pvlib/tests/test_shading.py index b7981cd02d..8d609d1e3f 100644 --- a/pvlib/tests/test_shading.py +++ b/pvlib/tests/test_shading.py @@ -2,28 +2,31 @@ import pandas as pd from pandas.testing import assert_series_equal +from numpy.testing import assert_allclose import pytest +from datetime import timezone, timedelta from pvlib import shading @pytest.fixture def test_system(): - syst = {'height': 1.0, - 'pitch': 2., - 'surface_tilt': 30., - 'surface_azimuth': 180., - 'rotation': -30.} # rotation of right edge relative to horizontal - syst['gcr'] = 1.0 / syst['pitch'] + syst = { + "height": 1.0, + "pitch": 2.0, + "surface_tilt": 30.0, + "surface_azimuth": 180.0, + "rotation": -30.0, + } # rotation of right edge relative to horizontal + syst["gcr"] = 1.0 / syst["pitch"] return syst def test__ground_angle(test_system): ts = test_system - x = np.array([0., 0.5, 1.0]) - angles = shading.ground_angle( - ts['surface_tilt'], ts['gcr'], x) - expected_angles = np.array([0., 5.866738789543952, 9.896090638982903]) + x = np.array([0.0, 0.5, 1.0]) + angles = shading.ground_angle(ts["surface_tilt"], ts["gcr"], x) + expected_angles = np.array([0.0, 5.866738789543952, 9.896090638982903]) assert np.allclose(angles, expected_angles) @@ -37,7 +40,7 @@ def test__ground_angle_zero_gcr(): @pytest.fixture def surface_tilt(): - idx = pd.date_range('2019-01-01', freq='h', periods=3) + idx = pd.date_range("2019-01-01", freq="h", periods=3) return pd.Series([0, 20, 90], index=idx) @@ -104,3 +107,119 @@ def test_sky_diffuse_passias_scalar(average_masking_angle, shading_loss): for angle, loss in zip(average_masking_angle, shading_loss): actual_loss = shading.sky_diffuse_passias(angle) assert np.isclose(loss, actual_loss) + + +@pytest.fixture +def true_tracking_angle_and_inputs_NREL(): + # data from NREL 'Slope-Aware Backtracking for Single-Axis Trackers' + # doi.org/10.2172/1660126 ; Accessed on 2023-11-06. + tzinfo = timezone(timedelta(hours=-5)) + axis_tilt_angle = 9.666 # deg + axis_azimuth_angle = 195.0 # deg + timedata = pd.DataFrame( + columns=("Apparent Elevation", "Solar Azimuth", "True-Tracking"), + data=( + (2.404287, 122.791770, -84.440), + (11.263058, 133.288729, -72.604), + (18.733558, 145.285552, -59.861), + (24.109076, 158.939435, -45.578), + (26.810735, 173.931802, -28.764), + (26.482495, 189.371536, -8.475), + (23.170447, 204.136810, 15.120), + (17.296785, 217.446538, 39.562), + (9.461862, 229.102218, 61.587), + (0.524817, 239.330401, 79.530), + ), + ) + timedata.index = pd.date_range( + "2019-01-01T08", "2019-01-01T17", freq="1H", tz=tzinfo + ) + timedata["Apparent Zenith"] = 90.0 - timedata["Apparent Elevation"] + return (axis_tilt_angle, axis_azimuth_angle, timedata) + + +@pytest.fixture +def projected_solar_zenith_angle_edge_cases(): + premises_and_result_matrix = pd.DataFrame( + data=[ + # s_zen | s_azm | ax_tilt | ax_azm | psza + [ 0, 0, 0, 0, 0], + [ 0, 180, 0, 0, 0], + [ 0, 0, 0, 180, 0], + [ 0, 180, 0, 180, 0], + [ 45, 0, 0, 180, 0], + [ 45, 90, 0, 180, -45], + [ 45, 270, 0, 180, 45], + [ 45, 90, 90, 180, -90], + [ 45, 270, 90, 180, 90], + [ 45, 90, 90, 0, 90], + [ 45, 270, 90, 0, -90], + [ 45, 45, 90, 180, -135], + [ 45, 315, 90, 180, 135], + ], + columns=["solar_zenith", "solar_azimuth", "axis_tilt", "axis_azimuth", + "psza"], + ) + return premises_and_result_matrix + + +def test_projected_solar_zenith_angle_numeric( + true_tracking_angle_and_inputs_NREL, + projected_solar_zenith_angle_edge_cases +): + psza_func = shading.projected_solar_zenith_angle + axis_tilt, axis_azimuth, timedata = true_tracking_angle_and_inputs_NREL + # test against data provided by NREL + psz = psza_func( + timedata["Apparent Zenith"], + timedata["Solar Azimuth"], + axis_tilt, + axis_azimuth, + ) + assert_allclose(psz, timedata["True-Tracking"], atol=1e-3) + # test by changing axis azimuth and tilt + psza = psza_func( + timedata["Apparent Zenith"], + timedata["Solar Azimuth"], + -axis_tilt, + axis_azimuth - 180, + ) + assert_allclose(psza, -timedata["True-Tracking"], atol=1e-3) + + # test edge cases + solar_zenith, solar_azimuth, axis_tilt, axis_azimuth, psza_expected = ( + v for _, v in projected_solar_zenith_angle_edge_cases.items() + ) + psza = psza_func( + solar_zenith, + solar_azimuth, + axis_tilt, + axis_azimuth, + ) + assert_allclose(psza, psza_expected, atol=1e-9) + + +@pytest.mark.parametrize( + "cast_type, cast_func", + [ + (float, lambda x: float(x)), + (np.ndarray, lambda x: np.array([x])), + (pd.Series, lambda x: pd.Series(data=[x])), + ], +) +def test_projected_solar_zenith_angle_datatypes( + cast_type, cast_func, true_tracking_angle_and_inputs_NREL +): + psz_func = shading.projected_solar_zenith_angle + axis_tilt, axis_azimuth, timedata = true_tracking_angle_and_inputs_NREL + sun_apparent_zenith = timedata["Apparent Zenith"].iloc[0] + sun_azimuth = timedata["Solar Azimuth"].iloc[0] + + axis_tilt, axis_azimuth, sun_apparent_zenith, sun_azimuth = ( + cast_func(sun_apparent_zenith), + cast_func(sun_azimuth), + cast_func(axis_tilt), + cast_func(axis_azimuth), + ) + psz = psz_func(sun_apparent_zenith, axis_azimuth, axis_tilt, axis_azimuth) + assert isinstance(psz, cast_type) diff --git a/pvlib/tracking.py b/pvlib/tracking.py index 04ed5f8506..9c4103e7f0 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -3,6 +3,7 @@ from pvlib.tools import cosd, sind, tand, acosd, asind from pvlib import irradiance +from pvlib import shading def singleaxis(apparent_zenith, apparent_azimuth, @@ -126,51 +127,20 @@ def singleaxis(apparent_zenith, apparent_azimuth, if apparent_azimuth.ndim > 1 or apparent_zenith.ndim > 1: raise ValueError('Input dimensions must not exceed 1') - # Calculate sun position x, y, z using coordinate system as in [1], Eq 1. - - # NOTE: solar elevation = 90 - solar zenith, then use trig identities: - # sin(90-x) = cos(x) & cos(90-x) = sin(x) - sin_zenith = sind(apparent_zenith) - x = sin_zenith * sind(apparent_azimuth) - y = sin_zenith * cosd(apparent_azimuth) - z = cosd(apparent_zenith) - - # Assume the tracker reference frame is right-handed. Positive y-axis is - # oriented along tracking axis; from north, the y-axis is rotated clockwise - # by the axis azimuth and tilted from horizontal by the axis tilt. The - # positive x-axis is 90 deg clockwise from the y-axis and parallel to - # horizontal (e.g., if the y-axis is south, the x-axis is west); the - # positive z-axis is normal to the x and y axes, pointed upward. - - # Calculate sun position (xp, yp, zp) in tracker coordinate system using - # [1] Eq 4. - - cos_axis_azimuth = cosd(axis_azimuth) - sin_axis_azimuth = sind(axis_azimuth) - cos_axis_tilt = cosd(axis_tilt) - sin_axis_tilt = sind(axis_tilt) - xp = x*cos_axis_azimuth - y*sin_axis_azimuth - # not necessary to calculate y' - # yp = (x*cos_axis_tilt*sin_axis_azimuth - # + y*cos_axis_tilt*cos_axis_azimuth - # - z*sin_axis_tilt) - zp = (x*sin_axis_tilt*sin_axis_azimuth - + y*sin_axis_tilt*cos_axis_azimuth - + z*cos_axis_tilt) - # The ideal tracking angle wid is the rotation to place the sun position - # vector (xp, yp, zp) in the (y, z) plane, which is normal to the panel and + # vector (xp, yp, zp) in the (x, z) plane, which is normal to the panel and # contains the axis of rotation. wid = 0 indicates that the panel is # horizontal. Here, our convention is that a clockwise rotation is # positive, to view rotation angles in the same frame of reference as # azimuth. For example, for a system with tracking axis oriented south, a # rotation toward the east is negative, and a rotation to the west is # positive. This is a right-handed rotation around the tracker y-axis. - - # Calculate angle from x-y plane to projection of sun vector onto x-z plane - # using [1] Eq. 5. - - wid = np.degrees(np.arctan2(xp, zp)) + wid = shading.projected_solar_zenith_angle( + axis_tilt=axis_tilt, + axis_azimuth=axis_azimuth, + solar_zenith=apparent_zenith, + solar_azimuth=apparent_azimuth, + ) # filter for sun above panel horizon zen_gt_90 = apparent_zenith > 90