From 6f63d3487f97603818bf0a23748e6f2aa2d4ec50 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 28 Jan 2021 17:33:28 -0700 Subject: [PATCH 1/7] add example --- .../plot_interval_transposition_error.py | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 docs/examples/plot_interval_transposition_error.py diff --git a/docs/examples/plot_interval_transposition_error.py b/docs/examples/plot_interval_transposition_error.py new file mode 100644 index 0000000000..813123d190 --- /dev/null +++ b/docs/examples/plot_interval_transposition_error.py @@ -0,0 +1,153 @@ +""" +Sun path diagram +================ + +Transposing interval-averaged irradiance data +""" + +# %% +# This example shows how not accounting for the difference between +# instantaneous and interval-averaged time series data can introduce +# error in the modeling process. An instantaneous time series +# represents discrete measurements taken at each timestamp, while +# an interval-averaged time series represents the average value across +# each data interval. For example, the value of an interval-averaged +# hourly time series at 11:00 represents the average value between +# 11:00 and 12:00 (if the series is left-labeled), the average value +# between 10:00 and 11:00 (if the series is right-labeled), or the +# average value between 10:30 and 11:30 (if the series is +# center-labeled). Interval-averaged time series are common in +# field data, where the datalogger averages high-frequency measurements +# into low-frequency averages for archiving purposes. +# +# It is important to account for this difference When using +# interval-averaged weather data for modeling. This example +# focuses on calculating solar position appropriately for +# irradiance transposition, but this concept is relevant for +# other steps in the modeling process as well. +# +# This example calculates a POA irradiance timeseries at 1-second +# resolution as a "ground truth" value. Then it performs the +# transposition again at lower resolution using interval-averaged +# irradiance components, once using a half-interval shift and +# once just using the unmodified timestamps. The difference +# affects the solar position calculation: for example, assuming +# we have average irradiance for the interval 11:00 to 12:00, +# and it came from a left-labeled time series, naively using +# the unmodified timestamp will calculate solar position for 11:00, +# meaning the calculated solar position is used to represent +# times as far as an hour away. A better option would be to +# calculate the solar position at 11:30 to reduce the maximum +# timing error to only half an hour. + +import pvlib +import pandas as pd +import matplotlib.pyplot as plt + +# %% +# First, we'll define a helper function that we can re-use several +# times in the following code: + + +def transpose(irradiance, timeshift): + """ + Transpose irradiance components to plane-of-array, incorporating + a timeshift in the solar position calculation. + + Inputs: + irradiance: DataFrame with columns dni, ghi, dhi + timeshift: float, minutes to shift for solar position calculation + Outputs: + Series of POA irradiance + """ + idx = irradiance.index + # calculate solar position for shifted timestamps: + idx = idx + pd.Timedelta(timeshift, unit='min') + solpos = location.get_solarposition(idx) + # but still report the values with the original timestamps: + solpos.index = irradiance.index + + poa_components = pvlib.irradiance.get_total_irradiance( + surface_tilt=20, + surface_azimuth=180, + solar_zenith=solpos['apparent_zenith'], + solar_azimuth=solpos['azimuth'], + dni=irradiance['dni'], + ghi=irradiance['ghi'], + dhi=irradiance['dhi'], + model='isotropic', + ) + return poa_components['poa_global'] + + +# %% +# Now, calculate the "ground truth" irradiance data. We'll simulate +# clear-sky irradiance components at 1-second intervals and calculate +# the corresponding POA irradiance. At such a short timescale, the +# difference between instantaneous and interval-averaged irradiance +# is negligible. + +# baseline: all calculations done at 1-second scale +location = pvlib.location.Location(40, -80, tz='Etc/GMT+5') +times = pd.date_range('2019-06-01 05:00', '2019-06-01 19:00', + freq='1s', tz='Etc/GMT+5') +solpos = location.get_solarposition(times) +clearsky = location.get_clearsky(times, solar_position=solpos) +poa_1s = transpose(clearsky, timeshift=0) # no shift needed for 1s data + +# %% +# Now, we will aggregate the 1-second values into interval averages. +# To see how the averaging interval affects results, we'll loop over +# a few common data intervals and accumulate the results. + +results = [] + +for timescale_minutes in [1, 5, 10, 15, 30, 60]: + + timescale_str = f'{timescale_minutes}min' + # get the "true" interval average of poa as the baseline for comparison + poa_avg = poa_1s.resample(timescale_str).mean() + # get interval averages of irradiance components to use for transposition + clearsky_avg = clearsky.resample(timescale_str).mean() + + # low-res interval averages of 1-second data, with NO shift + poa_avg_noshift = transpose(clearsky_avg, timeshift=0) + + # low-res interval averages of 1-second data, with half-interval shift + poa_avg_halfshift = transpose(clearsky_avg, timeshift=timescale_minutes/2) + + df = pd.DataFrame({ + 'ground truth': poa_avg, + 'modeled, no shift': poa_avg_noshift, + 'modeled, half shift': poa_avg_halfshift, + }) + # calculate error statistics and save for later + error = df.subtract(df['ground truth'], axis=0) + stats = error.abs().mean() + stats['timescale_minutes'] = timescale_minutes + results.append(stats) + +df_results = pd.DataFrame(results).set_index('timescale_minutes') +print(df_results) + +# %% +# The differences shown above are the absolute difference in $W/m^2$. +# In this example, using the timestamps unadjusted creates an error that +# increases with increasing interval length, up to a ~40% $W/m^2$ error +# at hourly resolution. In contrast, incorporating a half-interval shift +# so that solar position is calculated in the middle of the interval +# instead of the edge reduces the error by one or two orders of magnitude: + +df_results[['modeled, no shift', 'modeled, half shift']].plot.bar(rot=0) +plt.ylabel('Mean Absolute Error [W/m$^2$]') +plt.xlabel('Transposition Timescale [minutes]') + +# %% +# We can also plot the underlying time series results of the last +# iteration (hourly in this case). The modeled irradiance using +# no shift is effectively time-lagged compared with ground truth. +# In contrast, the half-shift model is nearly identical to the ground +# truth irradiance. + +ax = df.plot() +plt.ylabel('Irradiance [W/m$^2$]') From 487c96407c81a3244fa287daab1915f567d247cc Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 28 Jan 2021 17:36:54 -0700 Subject: [PATCH 2/7] whatsnew --- docs/sphinx/source/whatsnew/v0.9.0.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index 605898cf61..bd953d77ee 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -108,7 +108,10 @@ Documentation ~~~~~~~~~~~~~ * Update intro tutorial to highlight the use of historical meteorological data - and to make the procedural and OO results match exactly. + and to make the procedural and OO results match exactly. (:issue:`1116`, :pull:`1144`) +* Add a gallery example showing how to appropriately use interval-averaged + weather data for modeling. (:pull:`1152`) + Requirements ~~~~~~~~~~~~ From a4e412b133f12708f1b641fe6b82b4eda16c162a Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 28 Jan 2021 17:38:59 -0700 Subject: [PATCH 3/7] fix title --- docs/examples/plot_interval_transposition_error.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/examples/plot_interval_transposition_error.py b/docs/examples/plot_interval_transposition_error.py index 813123d190..fa5226cc11 100644 --- a/docs/examples/plot_interval_transposition_error.py +++ b/docs/examples/plot_interval_transposition_error.py @@ -1,6 +1,6 @@ """ -Sun path diagram -================ +Modeling with interval averages +=============================== Transposing interval-averaged irradiance data """ From a9ca27087b2d9b328c36e2d1a487205b69e2f2e5 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 28 Jan 2021 17:52:47 -0700 Subject: [PATCH 4/7] make plots more compact --- docs/examples/plot_interval_transposition_error.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/examples/plot_interval_transposition_error.py b/docs/examples/plot_interval_transposition_error.py index fa5226cc11..daebae8eb0 100644 --- a/docs/examples/plot_interval_transposition_error.py +++ b/docs/examples/plot_interval_transposition_error.py @@ -138,9 +138,11 @@ def transpose(irradiance, timeshift): # so that solar position is calculated in the middle of the interval # instead of the edge reduces the error by one or two orders of magnitude: -df_results[['modeled, no shift', 'modeled, half shift']].plot.bar(rot=0) +fig, ax = plt.subplots(figsize=(5, 3)) +df_results[['modeled, no shift', 'modeled, half shift']].plot.bar(rot=0, ax=ax) plt.ylabel('Mean Absolute Error [W/m$^2$]') plt.xlabel('Transposition Timescale [minutes]') +fig.tight_layout() # %% # We can also plot the underlying time series results of the last @@ -149,5 +151,7 @@ def transpose(irradiance, timeshift): # In contrast, the half-shift model is nearly identical to the ground # truth irradiance. -ax = df.plot() +fig, ax = plt.subplots(figsize=(5, 3)) +ax = df.plot(ax=ax) plt.ylabel('Irradiance [W/m$^2$]') +fig.tight_layout() From 2aac37a813becd7d191c62e784262c4d5deafbcc Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 28 Jan 2021 17:53:28 -0700 Subject: [PATCH 5/7] fix unrelated sphinx issue --- docs/sphinx/source/whatsnew/v0.9.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index bd953d77ee..682ca8fe3e 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -98,7 +98,7 @@ Enhancements Bug fixes ~~~~~~~~~ * Pass weather data to solar position calculations in - :ref:meth:`~pvlib.modelchain.ModelChain.prepare_inputs_from_poa`. + :py:meth:`~pvlib.modelchain.ModelChain.prepare_inputs_from_poa`. (:issue:`1065`, :pull:`1140`) Testing From f324f66e064db7fdceb90a1aae059d6c7e91ad63 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 28 Jan 2021 18:02:35 -0700 Subject: [PATCH 6/7] forgot this wasn't latex --- docs/examples/plot_interval_transposition_error.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/examples/plot_interval_transposition_error.py b/docs/examples/plot_interval_transposition_error.py index daebae8eb0..a3bd653b2c 100644 --- a/docs/examples/plot_interval_transposition_error.py +++ b/docs/examples/plot_interval_transposition_error.py @@ -131,9 +131,9 @@ def transpose(irradiance, timeshift): print(df_results) # %% -# The differences shown above are the absolute difference in $W/m^2$. +# The differences shown above are the absolute difference in :math:`W/m^2`. # In this example, using the timestamps unadjusted creates an error that -# increases with increasing interval length, up to a ~40% $W/m^2$ error +# increases with increasing interval length, up to a ~40% :math:`W/m^2` error # at hourly resolution. In contrast, incorporating a half-interval shift # so that solar position is calculated in the middle of the interval # instead of the edge reduces the error by one or two orders of magnitude: From 29945fd79680bedb794646ddd60c11039097c566 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 29 Jan 2021 09:17:44 -0700 Subject: [PATCH 7/7] updates from review --- .../plot_interval_transposition_error.py | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/docs/examples/plot_interval_transposition_error.py b/docs/examples/plot_interval_transposition_error.py index a3bd653b2c..76adfed932 100644 --- a/docs/examples/plot_interval_transposition_error.py +++ b/docs/examples/plot_interval_transposition_error.py @@ -6,21 +6,23 @@ """ # %% -# This example shows how not accounting for the difference between +# This example shows how failing to account for the difference between # instantaneous and interval-averaged time series data can introduce # error in the modeling process. An instantaneous time series # represents discrete measurements taken at each timestamp, while # an interval-averaged time series represents the average value across # each data interval. For example, the value of an interval-averaged # hourly time series at 11:00 represents the average value between -# 11:00 and 12:00 (if the series is left-labeled), the average value -# between 10:00 and 11:00 (if the series is right-labeled), or the -# average value between 10:30 and 11:30 (if the series is -# center-labeled). Interval-averaged time series are common in +# 11:00 (inclusive) and 12:00 (exclusive), assuming the series is left-labeled. +# For a right-labeled time series it would be the average value +# between 10:00 (exclusive) and 11:00 (inclusive). Sometimes timestamps +# are center-labeled, in which case it would be the +# average value between 10:30 and 11:30. +# Interval-averaged time series are common in # field data, where the datalogger averages high-frequency measurements # into low-frequency averages for archiving purposes. # -# It is important to account for this difference When using +# It is important to account for this difference when using # interval-averaged weather data for modeling. This example # focuses on calculating solar position appropriately for # irradiance transposition, but this concept is relevant for @@ -54,9 +56,12 @@ def transpose(irradiance, timeshift): Transpose irradiance components to plane-of-array, incorporating a timeshift in the solar position calculation. - Inputs: - irradiance: DataFrame with columns dni, ghi, dhi - timeshift: float, minutes to shift for solar position calculation + Parameters + ---------- + irradiance: DataFrame + Has columns dni, ghi, dhi + timeshift: float + Number of minutes to shift for solar position calculation Outputs: Series of POA irradiance """ @@ -100,6 +105,8 @@ def transpose(irradiance, timeshift): # To see how the averaging interval affects results, we'll loop over # a few common data intervals and accumulate the results. +fig, ax = plt.subplots(figsize=(5, 3)) + results = [] for timescale_minutes in [1, 5, 10, 15, 30, 60]: @@ -118,30 +125,36 @@ def transpose(irradiance, timeshift): df = pd.DataFrame({ 'ground truth': poa_avg, - 'modeled, no shift': poa_avg_noshift, 'modeled, half shift': poa_avg_halfshift, + 'modeled, no shift': poa_avg_noshift, }) - # calculate error statistics and save for later error = df.subtract(df['ground truth'], axis=0) - stats = error.abs().mean() + # add another trace to the error plot + error['modeled, no shift'].plot(ax=ax, label=timescale_str) + # calculate error statistics and save for later + stats = error.abs().mean() # average absolute error across daylight hours stats['timescale_minutes'] = timescale_minutes results.append(stats) +ax.legend(ncol=2) +ax.set_ylabel('Transposition Error [W/m$^2$]') +fig.tight_layout() + df_results = pd.DataFrame(results).set_index('timescale_minutes') print(df_results) # %% -# The differences shown above are the absolute difference in :math:`W/m^2`. +# The errors shown above are the average absolute difference in :math:`W/m^2`. # In this example, using the timestamps unadjusted creates an error that -# increases with increasing interval length, up to a ~40% :math:`W/m^2` error +# increases with increasing interval length, up to a ~40% error # at hourly resolution. In contrast, incorporating a half-interval shift # so that solar position is calculated in the middle of the interval # instead of the edge reduces the error by one or two orders of magnitude: fig, ax = plt.subplots(figsize=(5, 3)) df_results[['modeled, no shift', 'modeled, half shift']].plot.bar(rot=0, ax=ax) -plt.ylabel('Mean Absolute Error [W/m$^2$]') -plt.xlabel('Transposition Timescale [minutes]') +ax.set_ylabel('Mean Absolute Error [W/m$^2$]') +ax.set_xlabel('Transposition Timescale [minutes]') fig.tight_layout() # %% @@ -152,6 +165,6 @@ def transpose(irradiance, timeshift): # truth irradiance. fig, ax = plt.subplots(figsize=(5, 3)) -ax = df.plot(ax=ax) -plt.ylabel('Irradiance [W/m$^2$]') +ax = df.plot(ax=ax, style=[None, ':', None], lw=3) +ax.set_ylabel('Irradiance [W/m$^2$]') fig.tight_layout()