|
| 1 | +""" |
| 2 | +The ``shading`` module contains functions that model module shading and the |
| 3 | +associated effects on PV module output |
| 4 | +""" |
| 5 | + |
| 6 | +import numpy as np |
| 7 | +import pandas as pd |
| 8 | +from pvlib.tools import sind, cosd |
| 9 | + |
| 10 | + |
| 11 | +def masking_angle(surface_tilt, gcr, slant_height): |
| 12 | + """ |
| 13 | + The elevation angle below which diffuse irradiance is blocked. |
| 14 | +
|
| 15 | + The ``height`` parameter determines how far up the module's surface to |
| 16 | + evaluate the masking angle. The lower the point, the steeper the masking |
| 17 | + angle [1]_. SAM uses a "worst-case" approach where the masking angle |
| 18 | + is calculated for the bottom of the array (i.e. ``slant_height=0``) [2]_. |
| 19 | +
|
| 20 | + Parameters |
| 21 | + ---------- |
| 22 | + surface_tilt : numeric |
| 23 | + Panel tilt from horizontal [degrees]. |
| 24 | +
|
| 25 | + gcr : float |
| 26 | + The ground coverage ratio of the array [unitless]. |
| 27 | +
|
| 28 | + slant_height : numeric |
| 29 | + The distance up the module's slant height to evaluate the masking |
| 30 | + angle, as a fraction [0-1] of the module slant height [unitless]. |
| 31 | +
|
| 32 | + Returns |
| 33 | + ------- |
| 34 | + mask_angle : numeric |
| 35 | + Angle from horizontal where diffuse light is blocked by the |
| 36 | + preceding row [degrees]. |
| 37 | +
|
| 38 | + See Also |
| 39 | + -------- |
| 40 | + masking_angle_passias |
| 41 | + sky_diffuse_passias |
| 42 | +
|
| 43 | + References |
| 44 | + ---------- |
| 45 | + .. [1] D. Passias and B. Källbäck, "Shading effects in rows of solar cell |
| 46 | + panels", Solar Cells, Volume 11, Pages 281-291. 1984. |
| 47 | + DOI: 10.1016/0379-6787(84)90017-6 |
| 48 | + .. [2] Gilman, P. et al., (2018). "SAM Photovoltaic Model Technical |
| 49 | + Reference Update", NREL Technical Report NREL/TP-6A20-67399. |
| 50 | + Available at https://www.nrel.gov/docs/fy18osti/67399.pdf |
| 51 | + """ |
| 52 | + # The original equation (8 in [1]) requires pitch and collector width, |
| 53 | + # but it's easy to non-dimensionalize it to make it a function of GCR |
| 54 | + # by factoring out B from the argument to arctan. |
| 55 | + numerator = (1 - slant_height) * sind(surface_tilt) |
| 56 | + denominator = 1/gcr - (1 - slant_height) * cosd(surface_tilt) |
| 57 | + phi = np.arctan(numerator / denominator) |
| 58 | + return np.degrees(phi) |
| 59 | + |
| 60 | + |
| 61 | +def masking_angle_passias(surface_tilt, gcr): |
| 62 | + r""" |
| 63 | + The average masking angle over the slant height of a row. |
| 64 | +
|
| 65 | + The masking angle is the angle from horizontal where the sky dome is |
| 66 | + blocked by the row in front. The masking angle is larger near the lower |
| 67 | + edge of a row than near the upper edge. This function calculates the |
| 68 | + average masking angle as described in [1]_. |
| 69 | +
|
| 70 | + Parameters |
| 71 | + ---------- |
| 72 | + surface_tilt : numeric |
| 73 | + Panel tilt from horizontal [degrees]. |
| 74 | +
|
| 75 | + gcr : float |
| 76 | + The ground coverage ratio of the array [unitless]. |
| 77 | +
|
| 78 | + Returns |
| 79 | + ---------- |
| 80 | + mask_angle : numeric |
| 81 | + Average angle from horizontal where diffuse light is blocked by the |
| 82 | + preceding row [degrees]. |
| 83 | +
|
| 84 | + See Also |
| 85 | + -------- |
| 86 | + masking_angle |
| 87 | + sky_diffuse_passias |
| 88 | +
|
| 89 | + Notes |
| 90 | + ----- |
| 91 | + The pvlib-python authors believe that Eqn. 9 in [1]_ is incorrect. |
| 92 | + Here we use an independent equation. First, Eqn. 8 is non-dimensionalized |
| 93 | + (recasting in terms of GCR): |
| 94 | +
|
| 95 | + .. math:: |
| 96 | +
|
| 97 | + \psi(z') = \arctan \left [ |
| 98 | + \frac{(1 - z') \sin \beta} |
| 99 | + {\mathrm{GCR}^{-1} + (z' - 1) \cos \beta} |
| 100 | + \right ] |
| 101 | +
|
| 102 | + Where :math:`GCR = B/C` and :math:`z' = z/B`. The average masking angle |
| 103 | + :math:`\overline{\psi} = \int_0^1 \psi(z') \mathrm{d}z'` is then |
| 104 | + evaluated symbolically using Maxima (using :math:`X = 1/\mathrm{GCR}`): |
| 105 | +
|
| 106 | + .. code-block:: none |
| 107 | +
|
| 108 | + load(scifac) /* for the gcfac function */ |
| 109 | + assume(X>0, cos(beta)>0, cos(beta)-X<0); /* X is 1/GCR */ |
| 110 | + gcfac(integrate(atan((1-z)*sin(beta)/(X+(z-1)*cos(beta))), z, 0, 1)) |
| 111 | +
|
| 112 | + This yields the equation implemented by this function: |
| 113 | +
|
| 114 | + .. math:: |
| 115 | +
|
| 116 | + \overline{\psi} = \ |
| 117 | + &-\frac{X}{2} \sin\beta \log | 2 X \cos\beta - (X^2 + 1)| \\ |
| 118 | + &+ (X \cos\beta - 1) \arctan \frac{X \cos\beta - 1}{X \sin\beta} \\ |
| 119 | + &+ (1 - X \cos\beta) \arctan \frac{\cos\beta}{\sin\beta} \\ |
| 120 | + &+ X \log X \sin\beta |
| 121 | +
|
| 122 | + The pvlib-python authors have validated this equation against numerical |
| 123 | + integration of :math:`\overline{\psi} = \int_0^1 \psi(z') \mathrm{d}z'`. |
| 124 | +
|
| 125 | + References |
| 126 | + ---------- |
| 127 | + .. [1] D. Passias and B. Källbäck, "Shading effects in rows of solar cell |
| 128 | + panels", Solar Cells, Volume 11, Pages 281-291. 1984. |
| 129 | + DOI: 10.1016/0379-6787(84)90017-6 |
| 130 | + """ |
| 131 | + # wrap it in an array so that division by zero is handled well |
| 132 | + beta = np.radians(np.array(surface_tilt)) |
| 133 | + sin_b = np.sin(beta) |
| 134 | + cos_b = np.cos(beta) |
| 135 | + X = 1/gcr |
| 136 | + |
| 137 | + with np.errstate(divide='ignore', invalid='ignore'): # ignore beta=0 |
| 138 | + term1 = -X * sin_b * np.log(np.abs(2 * X * cos_b - (X**2 + 1))) / 2 |
| 139 | + term2 = (X * cos_b - 1) * np.arctan((X * cos_b - 1) / (X * sin_b)) |
| 140 | + term3 = (1 - X * cos_b) * np.arctan(cos_b / sin_b) |
| 141 | + term4 = X * np.log(X) * sin_b |
| 142 | + |
| 143 | + psi_avg = term1 + term2 + term3 + term4 |
| 144 | + # when beta=0, divide by zero makes psi_avg NaN. replace with 0: |
| 145 | + psi_avg = np.where(np.isfinite(psi_avg), psi_avg, 0) |
| 146 | + |
| 147 | + if isinstance(surface_tilt, pd.Series): |
| 148 | + psi_avg = pd.Series(psi_avg, index=surface_tilt.index) |
| 149 | + |
| 150 | + return np.degrees(psi_avg) |
| 151 | + |
| 152 | + |
| 153 | +def sky_diffuse_passias(masking_angle): |
| 154 | + r""" |
| 155 | + The diffuse irradiance loss caused by row-to-row sky diffuse shading. |
| 156 | +
|
| 157 | + Even when the sun is high in the sky, a row's view of the sky dome will |
| 158 | + be partially blocked by the row in front. This causes a reduction in the |
| 159 | + diffuse irradiance incident on the module. The reduction depends on the |
| 160 | + masking angle, the elevation angle from a point on the shaded module to |
| 161 | + the top of the shading row. In [1]_ the masking angle is calculated as |
| 162 | + the average across the module height. SAM assumes the "worst-case" loss |
| 163 | + where the masking angle is calculated for the bottom of the array [2]_. |
| 164 | +
|
| 165 | + This function, as in [1]_, makes the assumption that sky diffuse |
| 166 | + irradiance is isotropic. |
| 167 | +
|
| 168 | + Parameters |
| 169 | + ---------- |
| 170 | + masking_angle : numeric |
| 171 | + The elevation angle below which diffuse irradiance is blocked |
| 172 | + [degrees]. |
| 173 | +
|
| 174 | + Returns |
| 175 | + ------- |
| 176 | + derate : numeric |
| 177 | + The fraction [0-1] of blocked sky diffuse irradiance. |
| 178 | +
|
| 179 | + See Also |
| 180 | + -------- |
| 181 | + masking_angle |
| 182 | + masking_angle_passias |
| 183 | +
|
| 184 | + References |
| 185 | + ---------- |
| 186 | + .. [1] D. Passias and B. Källbäck, "Shading effects in rows of solar cell |
| 187 | + panels", Solar Cells, Volume 11, Pages 281-291. 1984. |
| 188 | + DOI: 10.1016/0379-6787(84)90017-6 |
| 189 | + .. [2] Gilman, P. et al., (2018). "SAM Photovoltaic Model Technical |
| 190 | + Reference Update", NREL Technical Report NREL/TP-6A20-67399. |
| 191 | + Available at https://www.nrel.gov/docs/fy18osti/67399.pdf |
| 192 | + """ |
| 193 | + return 1 - cosd(masking_angle/2)**2 |
0 commit comments