Skip to content

Implement reverse transposition using Perez-Driesse forward transposition #1907

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
662846e
Add reverse transposition function and two helpers.
adriesse Nov 12, 2023
24772a9
Add missing import.
adriesse Nov 12, 2023
60b103a
Add to docs under transposition until a better place is found/made.
adriesse Nov 12, 2023
2c4e4b9
Minor doc string fixes.
adriesse Nov 12, 2023
9bf9064
Add full_output option similar to newton().
adriesse Nov 13, 2023
1196851
First example for reverse transposition.
adriesse Nov 13, 2023
bf9105a
Second example for reverse transposition.
adriesse Nov 13, 2023
97d4559
Some improvements to the two examples.
adriesse Nov 13, 2023
17b3f3d
Placate flake8.
adriesse Nov 13, 2023
613e70b
Add tests for reverse transpostion.
adriesse Nov 14, 2023
e5025b4
Refine examples and fix test.
adriesse Nov 14, 2023
aaf34ba
Update whatsnew.
adriesse Nov 14, 2023
74f5c06
Refine examples.
adriesse Nov 14, 2023
58e8ded
Try to get rid of matplotlib warning in example.
adriesse Nov 14, 2023
0f35860
Remove unused import.
adriesse Nov 14, 2023
6e7ec7b
Update pvlib/irradiance.py
adriesse Nov 27, 2023
8106fcb
Improve examples based on reviews.
adriesse Nov 27, 2023
1c4da53
Settle conflict.
adriesse Nov 27, 2023
1c16709
Try again.
adriesse Nov 27, 2023
69d6288
Merge branch 'main' into rtranspose
adriesse Nov 27, 2023
8d7afa9
Remove one space.
adriesse Nov 27, 2023
d83e306
Merge branch 'main' into rtranspose
adriesse Dec 2, 2023
ae3cdb6
Final(?) changes.
adriesse Dec 2, 2023
d7fb73d
Update reference in erbs_driesse().
adriesse Dec 2, 2023
8021769
Fix links in examples.
adriesse Dec 2, 2023
cbf971b
Update docs/examples/irradiance-transposition/plot_rtranpose_limitati…
adriesse Dec 7, 2023
afcf178
Address review comments.
adriesse Dec 7, 2023
71db4c4
Final renames.
adriesse Dec 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions docs/examples/irradiance-transposition/plot_rtranpose_limitations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
"""
Reverse transposition limitations
====================================

Unfortunately, sometimes there is not a unique solution.

Author: Anton Driesse

"""

# %%
#
# Introduction
# ------------
# When irradiance is measured on a tilted plane, it is useful to be able to
# estimate the GHI that produces the POA irradiance.
# The estimation requires inverting a GHI-to-POA irradiance model,
# which involves two parts:
# a decomposition of GHI into direct and diffuse components,
# and a transposition model that calculates the direct and diffuse irradiance
# on the tilted plane.
# Recovering GHI from POA irradiance is termed "reverse transposition."
#
# Unfortunately, for a given POA irradiance value, sometimes there is not a
# unique solution for GHI.
# Different GHI values can produce different combinations of direct and
# diffuse irradiance that sum to the same POA irradiance value.
#
# In this example we look at a single point in time and consider a full range
# of possible GHI and POA global values as shown in figures 3 and 4 of [1]_.
# Then we use :py:meth:`pvlib.irradiance.ghi_from_poa_driesse_2023` to estimate
# the original GHI from POA global.
#
# References
# ----------
# .. [1] Driesse, A., Jensen, A., Perez, R., 2024. A Continuous form of the
# Perez diffuse sky model for forward and reverse transposition.
# Solar Energy vol. 267. :doi:`10.1016/j.solener.2023.112093`
#

import numpy as np

import matplotlib
import matplotlib.pyplot as plt

from pvlib.irradiance import (erbs_driesse,
get_total_irradiance,
ghi_from_poa_driesse_2023,
)

matplotlib.rcParams['axes.grid'] = True

# %%
#
# Define the conditions that were used for figure 3 in [1]_.
#

dni_extra = 1366.1
albedo = 0.25
surface_tilt = 40
surface_azimuth = 180

solar_azimuth = 82
solar_zenith = 75

# %%
#
# Define a range of possible GHI values and calculate the corresponding
# POA global. First estimate DNI and DHI using the Erbs-Driesse model, then
# transpose using the Perez-Driesse model.
#

ghi = np.linspace(0, 500, 100+1)

erbsout = erbs_driesse(ghi, solar_zenith, dni_extra=dni_extra)

dni = erbsout['dni']
dhi = erbsout['dhi']

irrads = get_total_irradiance(surface_tilt, surface_azimuth,
solar_zenith, solar_azimuth,
dni, ghi, dhi,
dni_extra,
model='perez-driesse')

poa_global = irrads['poa_global']

# %%
#
# Suppose you measure that POA global is 200 W/m2. What would GHI be?
#

poa_test = 200

ghi_hat = ghi_from_poa_driesse_2023(surface_tilt, surface_azimuth,
solar_zenith, solar_azimuth,
poa_test,
dni_extra,
full_output=False)

print('Estimated GHI: %.2f W/m².' % ghi_hat)

# %%
#
# Show this result on the graph of all possible combinations of GHI and POA.
#

plt.figure()
plt.plot(ghi, poa_global, 'k-')
plt.axvline(ghi_hat, color='g', lw=1)
plt.axhline(poa_test, color='g', lw=1)
plt.plot(ghi_hat, poa_test, 'gs')
plt.annotate('GHI=%.2f' % (ghi_hat),
xy=(ghi_hat-2, 200+2),
xytext=(ghi_hat-20, 200+20),
ha='right',
arrowprops={'arrowstyle': 'simple'})
plt.xlim(0, 500)
plt.ylim(0, 250)
plt.xlabel('GHI [W/m²]')
plt.ylabel('POA [W/m²]')
plt.show()

# %%
#
# Now change the solar azimuth to match the conditions for figure 4 in [1]_.
#

solar_azimuth = 76

# %%
#
# Again, estimate DNI and DHI using the Erbs-Driesse model, then
# transpose using the Perez-Driesse model.
#

erbsout = erbs_driesse(ghi, solar_zenith, dni_extra=dni_extra)

dni = erbsout['dni']
dhi = erbsout['dhi']

irrads = get_total_irradiance(surface_tilt, surface_azimuth,
solar_zenith, solar_azimuth,
dni, ghi, dhi,
dni_extra,
model='perez-driesse')

poa_global = irrads['poa_global']

# %%
#
# Now reverse transpose all the POA values and observe that the original
# GHI cannot always be found. There is a range of POA values that
# maps to three possible GHI values, and there is not enough information
# to choose one of them. Sometimes we get lucky and the right one comes
# out, other times not.
#

result = ghi_from_poa_driesse_2023(surface_tilt, surface_azimuth,
solar_zenith, solar_azimuth,
poa_global,
dni_extra,
full_output=True,
)

ghi_hat, conv, niter = result
correct = np.isclose(ghi, ghi_hat, atol=0.01)

plt.figure()
plt.plot(np.where(correct, ghi, np.nan), np.where(correct, poa_global, np.nan),
'g.', label='correct GHI found')
plt.plot(ghi[~correct], poa_global[~correct], 'r.', label='unreachable GHI')
plt.plot(ghi[~conv], poa_global[~conv], 'm.', label='out of range (kt > 1.25)')
plt.axhspan(88, 103, color='y', alpha=0.25, label='problem region')

plt.xlim(0, 500)
plt.ylim(0, 250)
plt.xlabel('GHI [W/m²]')
plt.ylabel('POA [W/m²]')
plt.legend()
plt.show()
152 changes: 152 additions & 0 deletions docs/examples/irradiance-transposition/plot_rtranpose_year.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"""
Reverse transposition using one year of hourly data
===================================================

With a brief look at accuracy and speed.

Author: Anton Driesse

"""
# %%
#
# Introduction
# ------------
# When irradiance is measured on a tilted plane, it is useful to be able to
# estimate the GHI that produces the POA irradiance.
# The estimation requires inverting a GHI-to-POA irradiance model,
# which involves two parts:
# a decomposition of GHI into direct and diffuse components,
# and a transposition model that calculates the direct and diffuse
# irradiance on the tilted plane.
# Recovering GHI from POA irradiance is termed "reverse transposition."
#
# In this example we start with a TMY file and calculate POA global irradiance.
# Then we use :py:meth:`pvlib.irradiance.ghi_from_poa_driesse_2023` to estimate
# the original GHI from POA global. Details of the method found in [1]_.
#
# Another method for reverse tranposition called GTI-DIRINT is also
# available in pvlib python (:py:meth:`pvlib.irradiance.gti_dirint`).
# More information is available in [2]_.
#
# References
# ----------
# .. [1] Driesse, A., Jensen, A., Perez, R., 2024. A Continuous form of the
# Perez diffuse sky model for forward and reverse transposition.
# Solar Energy vol. 267. :doi:`10.1016/j.solener.2023.112093`
#
# .. [2] B. Marion, A model for deriving the direct normal and
# diffuse horizontal irradiance from the global tilted
# irradiance, Solar Energy 122, 1037-1046.
# :doi:`10.1016/j.solener.2015.10.024`

import os
import time
import pandas as pd

import matplotlib.pyplot as plt

import pvlib
from pvlib import iotools, location
from pvlib.irradiance import (get_extra_radiation,
get_total_irradiance,
ghi_from_poa_driesse_2023,
aoi,
)

# %%
#
# Read a TMY3 file containing weather data and select needed columns.
#

PVLIB_DIR = pvlib.__path__[0]
DATA_FILE = os.path.join(PVLIB_DIR, 'data', '723170TYA.CSV')

tmy, metadata = iotools.read_tmy3(DATA_FILE, coerce_year=1990,
map_variables=True)

df = pd.DataFrame({'ghi': tmy['ghi'], 'dhi': tmy['dhi'], 'dni': tmy['dni'],
'temp_air': tmy['temp_air'],
'wind_speed': tmy['wind_speed'],
})

# %%
#
# Shift the timestamps to the middle of the hour and calculate sun positions.
#

df.index = df.index - pd.Timedelta(minutes=30)

loc = location.Location.from_tmy(metadata)
solpos = loc.get_solarposition(df.index)

# %%
#
# Estimate global irradiance on a fixed-tilt array (forward transposition).
# The array is tilted 30 degrees and oriented 30 degrees east of south.
#

TILT = 30
ORIENT = 150

df['dni_extra'] = get_extra_radiation(df.index)

total_irrad = get_total_irradiance(TILT, ORIENT,
solpos.apparent_zenith,
solpos.azimuth,
df.dni, df.ghi, df.dhi,
dni_extra=df.dni_extra,
model='perez-driesse')

df['poa_global'] = total_irrad.poa_global
df['aoi'] = aoi(TILT, ORIENT, solpos.apparent_zenith, solpos.azimuth)

# %%
#
# Now estimate ghi from poa_global using reverse transposition.
# The algorithm uses a simple bisection search, which is quite slow
# because scipy doesn't offer a vectorized version (yet).
# For this reason we'll process a random sample of 1000 timestamps
# rather than the whole year.
#

df = df[df.ghi > 0].sample(n=1000)
solpos = solpos.reindex(df.index)

start = time.process_time()

df['ghi_rev'] = ghi_from_poa_driesse_2023(TILT, ORIENT,
solpos.apparent_zenith,
solpos.azimuth,
df.poa_global,
dni_extra=df.dni_extra)
finish = time.process_time()

print('Elapsed time for reverse transposition: %.1f s' % (finish - start))

# %%
#
# This graph shows the reverse transposed values vs. the original values.
# The markers are color-coded by angle-of-incidence to show that
# errors occur primarily with incidence angle approaching 90° and beyond.
#
# Note that the results look particularly good because the POA values
# were calculated using the same models as used in reverse transposition.
# This isn't cheating though. It's a way of ensuring that the errors
# we see are really due to the reverse transposition algorithm.
# Expect to see larger errors with real-word POA measurements
# because errors from forward and reverse transposition will both be present.
#

df = df.sort_values('aoi')

plt.figure()
plt.gca().grid(True, alpha=.5)
pc = plt.scatter(df['ghi'], df['ghi_rev'], c=df['aoi'], s=15,
cmap='jet', vmin=60, vmax=120)
plt.colorbar(label='AOI [°]')
pc.set_alpha(0.5)

plt.xlabel('GHI original [W/m²]')
plt.ylabel('GHI from POA [W/m²]')

plt.show()
1 change: 1 addition & 0 deletions docs/sphinx/source/reference/irradiance/transposition.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ Transposition models
irradiance.klucher
irradiance.reindl
irradiance.king
irradiance.ghi_from_poa_driesse_2023
5 changes: 5 additions & 0 deletions docs/sphinx/source/whatsnew/v0.10.3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ Enhancements
~~~~~~~~~~~~
* Added the continuous Perez-Driesse transposition model.
:py:func:`pvlib.irradiance.perez_driesse` (:issue:`1841`, :pull:`1876`)
* Added a reverse transposition algorithm using the Perez-Driesse model.
:py:func:`pvlib.irradiance.ghi_from_poa_driesse_2023`
(:issue:`1901`, :pull:`1907`)
* :py:func:`pvlib.bifacial.infinite_sheds.get_irradiance` and
:py:func:`pvlib.bifacial.infinite_sheds.get_irradiance_poa` now include
shaded fraction in returned variables. (:pull:`1871`)
Expand All @@ -27,8 +30,10 @@ Documentation
~~~~~~~~~~~~~
* Create :ref:`weatherdata` User's Guide page. (:pull:`1754`)
* Fixed a plotting issue in the IV curve gallery example (:pull:`1895`)
* Added two examples to demonstrate reverse transposition (:pull:`1907`)
* Fixed `detect_clearsky` example in `clearsky.rst` (:issue:`1914`)


Requirements
~~~~~~~~~~~~
* Minimum version of scipy advanced from 1.4.0 to 1.5.0. (:issue:`1918`, :pull:`1919`)
Expand Down
Loading