|
| 1 | +""" |
| 2 | +Modeling Transposition Gain |
| 3 | +=========================== |
| 4 | +
|
| 5 | +Calculating the gain in insolation of a tilted module over a flat module. |
| 6 | +""" |
| 7 | + |
| 8 | +# %% |
| 9 | +# This example shows how to evaluate the transposition gain of a racking |
| 10 | +# strategy. The transposition gain is the additional insolation collected |
| 11 | +# by orienting at a tilt instead of horizontal; using PV modeling lingo, it's |
| 12 | +# the increase in POA (plane of array) insolation over GHI (global horizontal |
| 13 | +# irradiance) insolation. |
| 14 | +# |
| 15 | +# This example uses a TMY dataset and the |
| 16 | +# :py:meth:`pvlib.irradiance.get_total_irradiance` function to transpose |
| 17 | +# irradiance components to POA irradiance for various fixed tilts. It also |
| 18 | +# models a single-axis tracking system for comparison. The monthly POA |
| 19 | +# insolation is calculated for each strategy to show how orientation affects |
| 20 | +# seasonal irradiance collection. |
| 21 | + |
| 22 | +import pvlib |
| 23 | +from pvlib import location |
| 24 | +from pvlib import irradiance |
| 25 | +from pvlib import tracking |
| 26 | +from pvlib.iotools import read_tmy3 |
| 27 | +import pandas as pd |
| 28 | +from matplotlib import pyplot as plt |
| 29 | +import pathlib |
| 30 | + |
| 31 | +# get full path to the data directory |
| 32 | +DATA_DIR = pathlib.Path(pvlib.__file__).parent / 'data' |
| 33 | + |
| 34 | +# get TMY3 dataset |
| 35 | +tmy, metadata = read_tmy3(DATA_DIR / '723170TYA.CSV', coerce_year=1990) |
| 36 | +# TMY3 datasets are right-labeled (AKA "end of interval") which means the last |
| 37 | +# interval of Dec 31, 23:00 to Jan 1 00:00 is labeled Jan 1 00:00. When rolling |
| 38 | +# up hourly irradiance to monthly insolation, a spurious January value is |
| 39 | +# calculated from that last row, so we'll just go ahead and drop it here: |
| 40 | +tmy = tmy.iloc[:-1, :] |
| 41 | + |
| 42 | +# create location object to store lat, lon, timezone |
| 43 | +location = location.Location.from_tmy(metadata) |
| 44 | + |
| 45 | +# calculate the necessary variables to do transposition. Note that solar |
| 46 | +# position doesn't depend on array orientation, so we just calculate it once. |
| 47 | +# Note also that TMY datasets are right-labeled hourly intervals, e.g. the |
| 48 | +# 10AM to 11AM interval is labeled 11. We should calculate solar position in |
| 49 | +# the middle of the interval (10:30), so we subtract 30 minutes: |
| 50 | +times = tmy.index - pd.Timedelta('30min') |
| 51 | +solar_position = location.get_solarposition(times) |
| 52 | +# but remember to shift the index back to line up with the TMY data: |
| 53 | +solar_position.index += pd.Timedelta('30min') |
| 54 | + |
| 55 | + |
| 56 | +# create a helper function to do the transposition for us |
| 57 | +def calculate_poa(tmy, solar_position, surface_tilt, surface_azimuth): |
| 58 | + # Use the get_total_irradiance function to transpose the irradiance |
| 59 | + # components to POA irradiance |
| 60 | + poa = irradiance.get_total_irradiance( |
| 61 | + surface_tilt=surface_tilt, |
| 62 | + surface_azimuth=surface_azimuth, |
| 63 | + dni=tmy['DNI'], |
| 64 | + ghi=tmy['GHI'], |
| 65 | + dhi=tmy['DHI'], |
| 66 | + solar_zenith=solar_position['apparent_zenith'], |
| 67 | + solar_azimuth=solar_position['azimuth'], |
| 68 | + model='isotropic') |
| 69 | + return poa['poa_global'] # just return the total in-plane irradiance |
| 70 | + |
| 71 | + |
| 72 | +# create a dataframe to keep track of our monthly insolations |
| 73 | +df_monthly = pd.DataFrame() |
| 74 | + |
| 75 | +# fixed-tilt: |
| 76 | +for tilt in range(0, 50, 10): |
| 77 | + # we will hardcode azimuth=180 (south) for all fixed-tilt cases |
| 78 | + poa_irradiance = calculate_poa(tmy, solar_position, tilt, 180) |
| 79 | + column_name = "FT-{}".format(tilt) |
| 80 | + # TMYs are hourly, so we can just sum up irradiance [W/m^2] to get |
| 81 | + # insolation [Wh/m^2]: |
| 82 | + df_monthly[column_name] = poa_irradiance.resample('m').sum() |
| 83 | + |
| 84 | +# single-axis tracking: |
| 85 | +orientation = tracking.singleaxis(solar_position['apparent_zenith'], |
| 86 | + solar_position['azimuth'], |
| 87 | + axis_tilt=0, # flat array |
| 88 | + axis_azimuth=180, # south-facing azimuth |
| 89 | + max_angle=60, # a common maximum rotation |
| 90 | + backtrack=True, # backtrack for a c-Si array |
| 91 | + gcr=0.4) # a common ground coverage ratio |
| 92 | + |
| 93 | +poa_irradiance = calculate_poa(tmy, |
| 94 | + solar_position, |
| 95 | + orientation['surface_tilt'], |
| 96 | + orientation['surface_azimuth']) |
| 97 | +df_monthly['SAT-0.4'] = poa_irradiance.resample('m').sum() |
| 98 | + |
| 99 | +# calculate the percent difference from GHI |
| 100 | +ghi_monthly = tmy['GHI'].resample('m').sum() |
| 101 | +df_monthly = 100 * (df_monthly.divide(ghi_monthly, axis=0) - 1) |
| 102 | + |
| 103 | +df_monthly.plot() |
| 104 | +plt.xlabel('Month of Year') |
| 105 | +plt.ylabel('Monthly Transposition Gain [%]') |
| 106 | +plt.show() |
| 107 | + |
| 108 | + |
| 109 | +# %% |
| 110 | +# Note that in summer, steeper tilts actually collect less insolation than |
| 111 | +# flatter tilts because the sun is so high in the sky at solar noon. However, |
| 112 | +# the steeper tilts significantly increase insolation capture in winter when |
| 113 | +# the sun is lower in the sky. In contrast to the highly seasonal gain shown |
| 114 | +# by fixed tilts, the tracker system shows a much more consistent gain |
| 115 | +# year-round. |
| 116 | +# |
| 117 | +# Because the seasonality of the fixed-tilt transposition gain is driven by |
| 118 | +# solar position angles, the relative behavior of different orientations will |
| 119 | +# be different for different locations. For example, a common rule of thumb |
| 120 | +# (considered somewhat outdated today) used to be to set tilt equal to the |
| 121 | +# latitude of the system location. At higher latitudes, the sun doesn't get |
| 122 | +# as high in the sky, so steeper tilts make more sense. |
0 commit comments