|
| 1 | +""" |
| 2 | +Shaded fraction of a horizontal single-axis tracker |
| 3 | +==================================================== |
| 4 | +
|
| 5 | +This example illustrates how to calculate the 1D shaded fraction of three rows |
| 6 | +in a North-South horizontal single axis tracker (HSAT) configuration. |
| 7 | +""" |
| 8 | + |
| 9 | +# %% |
| 10 | +# :py:func:`pvlib.shading.shaded_fraction1d` exposes a useful method for the |
| 11 | +# calculation of the shaded fraction of the width of a solar collector. Here, |
| 12 | +# the width is defined as the dimension perpendicular to the axis of rotation. |
| 13 | +# This method for calculating the shaded fraction only requires minor |
| 14 | +# modifications to be applicable for fixed-tilt systems. |
| 15 | +# |
| 16 | +# It is highly recommended to read :py:func:`pvlib.shading.shaded_fraction1d` |
| 17 | +# documentation to understand the input parameters and the method's |
| 18 | +# capabilities. For more in-depth information, please see the journal paper |
| 19 | +# `10.1063/5.0202220 <https://doi.org/10.1063/5.0202220>`_ describing |
| 20 | +# the methodology. |
| 21 | +# |
| 22 | +# Let's start by obtaining the true-tracking angles for each of the rows and |
| 23 | +# limiting the angles to the range of -50 to 50 degrees. This decision is |
| 24 | +# done to create an example scenario with significant shading. |
| 25 | +# |
| 26 | +# Key functions used in this example are: |
| 27 | +# |
| 28 | +# 1. :py:func:`pvlib.tracking.singleaxis` to calculate the tracking angles. |
| 29 | +# 2. :py:func:`pvlib.shading.projected_solar_zenith_angle` to calculate the |
| 30 | +# projected solar zenith angle. |
| 31 | +# 3. :py:func:`pvlib.shading.shaded_fraction1d` to calculate the shaded |
| 32 | +# fractions. |
| 33 | +# |
| 34 | +# .. sectionauthor:: Echedey Luis <echelual (at) gmail.com> |
| 35 | + |
| 36 | +import pvlib |
| 37 | + |
| 38 | +import numpy as np |
| 39 | +import pandas as pd |
| 40 | +import matplotlib.pyplot as plt |
| 41 | +from matplotlib.dates import DateFormatter |
| 42 | + |
| 43 | +# Define the solar system parameters |
| 44 | +latitude, longitude = 28.51, -13.89 |
| 45 | +altitude = pvlib.location.lookup_altitude(latitude, longitude) |
| 46 | + |
| 47 | +axis_tilt = 3 # degrees, positive is upwards in the axis_azimuth direction |
| 48 | +axis_azimuth = 180 # degrees, N-S tracking axis |
| 49 | +collector_width = 3.2 # m |
| 50 | +pitch = 4.15 # m |
| 51 | +gcr = collector_width / pitch |
| 52 | +cross_axis_slope = -5 # degrees |
| 53 | +surface_to_axis_offset = 0.07 # m |
| 54 | + |
| 55 | +# Generate a time range for the simulation |
| 56 | +times = pd.date_range( |
| 57 | + start="2024-01-01T05", |
| 58 | + end="2024-01-01T21", |
| 59 | + freq="5min", |
| 60 | + tz="Atlantic/Canary", |
| 61 | +) |
| 62 | + |
| 63 | +# Calculate the solar position |
| 64 | +solar_position = pvlib.solarposition.get_solarposition( |
| 65 | + times, latitude, longitude, altitude |
| 66 | +) |
| 67 | +solar_azimuth = solar_position["azimuth"] |
| 68 | +solar_zenith = solar_position["apparent_zenith"] |
| 69 | + |
| 70 | +# Calculate the tracking angles |
| 71 | +rotation_angle = pvlib.tracking.singleaxis( |
| 72 | + solar_zenith, |
| 73 | + solar_azimuth, |
| 74 | + axis_tilt, |
| 75 | + axis_azimuth, |
| 76 | + max_angle=(-50, 50), # (min, max) degrees |
| 77 | + backtrack=False, |
| 78 | + gcr=gcr, |
| 79 | + cross_axis_tilt=cross_axis_slope, |
| 80 | +)["tracker_theta"] |
| 81 | + |
| 82 | +# %% |
| 83 | +# Once the tracker angles have been obtained, the next step is to calculate |
| 84 | +# the shaded fraction. Special care must be taken |
| 85 | +# to ensure that the shaded or shading tracker roles are correctly assigned |
| 86 | +# depending on the solar position. |
| 87 | +# This means we will have a result for each row, ``eastmost_shaded_fraction``, |
| 88 | +# ``middle_shaded_fraction``, and ``westmost_shaded_fraction``. |
| 89 | +# Switching the parameters will be based on the |
| 90 | +# sign of :py:func:`pvlib.shading.projected_solar_zenith_angle`. |
| 91 | +# |
| 92 | +# The following code is verbose to make it easier to understand the process, |
| 93 | +# but with some effort you may be able to simplify it. This verbosity also |
| 94 | +# allows to change the premises easily per case, e.g., in case of a tracker |
| 95 | +# failure or with a different system configuration. |
| 96 | + |
| 97 | +psza = pvlib.shading.projected_solar_zenith_angle( |
| 98 | + solar_zenith, solar_azimuth, axis_tilt, axis_azimuth |
| 99 | +) |
| 100 | + |
| 101 | +# Calculate the shaded fraction for the eastmost row |
| 102 | +eastmost_shaded_fraction = np.where( |
| 103 | + psza < 0, |
| 104 | + 0, # no shaded fraction in the morning |
| 105 | + # shaded fraction in the evening |
| 106 | + pvlib.shading.shaded_fraction1d( |
| 107 | + solar_zenith, |
| 108 | + solar_azimuth, |
| 109 | + axis_azimuth, |
| 110 | + shaded_row_rotation=rotation_angle, |
| 111 | + axis_tilt=axis_tilt, |
| 112 | + collector_width=collector_width, |
| 113 | + pitch=pitch, |
| 114 | + surface_to_axis_offset=surface_to_axis_offset, |
| 115 | + cross_axis_slope=cross_axis_slope, |
| 116 | + shading_row_rotation=rotation_angle, |
| 117 | + ), |
| 118 | +) |
| 119 | + |
| 120 | +# Calculate the shaded fraction for the middle row |
| 121 | +middle_shaded_fraction = np.where( |
| 122 | + psza < 0, |
| 123 | + # shaded fraction in the morning |
| 124 | + pvlib.shading.shaded_fraction1d( |
| 125 | + solar_zenith, |
| 126 | + solar_azimuth, |
| 127 | + axis_azimuth, |
| 128 | + shaded_row_rotation=rotation_angle, |
| 129 | + axis_tilt=axis_tilt, |
| 130 | + collector_width=collector_width, |
| 131 | + pitch=pitch, |
| 132 | + surface_to_axis_offset=surface_to_axis_offset, |
| 133 | + cross_axis_slope=cross_axis_slope, |
| 134 | + shading_row_rotation=rotation_angle, |
| 135 | + ), |
| 136 | + # shaded fraction in the evening |
| 137 | + pvlib.shading.shaded_fraction1d( |
| 138 | + solar_zenith, |
| 139 | + solar_azimuth, |
| 140 | + axis_azimuth, |
| 141 | + shaded_row_rotation=rotation_angle, |
| 142 | + axis_tilt=axis_tilt, |
| 143 | + collector_width=collector_width, |
| 144 | + pitch=pitch, |
| 145 | + surface_to_axis_offset=surface_to_axis_offset, |
| 146 | + cross_axis_slope=cross_axis_slope, |
| 147 | + shading_row_rotation=rotation_angle, |
| 148 | + ), |
| 149 | +) |
| 150 | + |
| 151 | +# Calculate the shaded fraction for the westmost row |
| 152 | +westmost_shaded_fraction = np.where( |
| 153 | + psza < 0, |
| 154 | + # shaded fraction in the morning |
| 155 | + pvlib.shading.shaded_fraction1d( |
| 156 | + solar_zenith, |
| 157 | + solar_azimuth, |
| 158 | + axis_azimuth, |
| 159 | + shaded_row_rotation=rotation_angle, |
| 160 | + axis_tilt=axis_tilt, |
| 161 | + collector_width=collector_width, |
| 162 | + pitch=pitch, |
| 163 | + surface_to_axis_offset=surface_to_axis_offset, |
| 164 | + cross_axis_slope=cross_axis_slope, |
| 165 | + shading_row_rotation=rotation_angle, |
| 166 | + ), |
| 167 | + 0, # no shaded fraction in the evening |
| 168 | +) |
| 169 | + |
| 170 | +# %% |
| 171 | +# Plot the shaded fraction result for each row: |
| 172 | +plt.plot(times, eastmost_shaded_fraction, label="East-most", color="blue") |
| 173 | +plt.plot(times, middle_shaded_fraction, label="Middle", color="green", |
| 174 | + linewidth=3, linestyle="--") # fmt: skip |
| 175 | +plt.plot(times, westmost_shaded_fraction, label="West-most", color="red") |
| 176 | +plt.title(r"$shaded\_fraction1d$ of each row vs time") |
| 177 | +plt.xlabel("Time") |
| 178 | +plt.gca().xaxis.set_major_formatter(DateFormatter("%H:%M")) |
| 179 | +plt.ylabel("Shaded Fraction") |
| 180 | +plt.legend() |
| 181 | +plt.show() |
0 commit comments