Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add functions to fit and convert IAM models (#1827)
* Adding functions, docstrings draft * Updating imports, adding docstrings * Updated rst file * Make linter edits * Docstring experiment * Added tests * More linter edits * Docstrings and linter edits * Docstrings and linter * LINTER * Docstrings edit * Added more tests * Annihilate spaces * Spacing * Changed default weight function * Silence numpy warning * Updating tests to work with new default * Forgot a comment * Return dict contains scalars now, instead of arrays * Adding option to not fix n * Adding straggler tests * Removing examples specific to old default weight function * Linter nitpicks * Update docstrings * Experimenting with example * Adjusting figure size * Edit gallery example * Fixing bounds * Linter * Example experimentation * exact ashrae intercept * editing docstrings mostly * whatsnew * fix errors * remove test for weight function size * editing * simplify weight function * improve martin_ruiz to physical, generalize tests * fix examples, split convert and fit examples * linter, improve coverage * spacing * fix reverse order test * improve examples * print parameters * whatsnew * remove v0.10.2 whatsnew * Revert "remove v0.10.2 whatsnew" This reverts commit ed35731. * put v0.10.2.rst right again * require scipy>=1.5.0 * linter * linter * suggestions from review * add reference * edits to examples * add note to convert * edit note on convert * edit both notes * polish the notes * sum not Sum * edits * remove test for scipy * edits from review * its not it's * change internal linspace to one degree intervals * use linspace(0, 90, 91) --------- Co-authored-by: Cliff Hansen <cwhanse@sandia.gov>
- Loading branch information
Showing
7 changed files
with
791 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
|
||
""" | ||
IAM Model Conversion | ||
==================== | ||
Illustrates how to convert from one IAM model to a different model using | ||
:py:func:`~pvlib.iam.convert`. | ||
""" | ||
|
||
# %% | ||
# An incidence angle modifier (IAM) model quantifies the fraction of direct | ||
# irradiance that is reflected away from a module's surface. Three popular | ||
# IAM models are Martin-Ruiz :py:func:`~pvlib.iam.martin_ruiz`, physical | ||
# :py:func:`~pvlib.iam.physical`, and ASHRAE :py:func:`~pvlib.iam.ashrae`. | ||
# Each model requires one or more parameters. | ||
# | ||
# Here, we show how to use | ||
# :py:func:`~pvlib.iam.convert` to estimate parameters for a desired target | ||
# IAM model from a source IAM model. Model conversion uses a weight | ||
# function that can assign more influence to some AOI values than others. | ||
# We illustrate how to provide a custom weight function to | ||
# :py:func:`~pvlib.iam.convert`. | ||
|
||
import numpy as np | ||
import matplotlib.pyplot as plt | ||
|
||
from pvlib.tools import cosd | ||
from pvlib.iam import (ashrae, martin_ruiz, physical, convert) | ||
|
||
# %% | ||
# Converting from one IAM model to another model | ||
# ---------------------------------------------- | ||
# | ||
# Here we'll show how to convert from the Martin-Ruiz model to the | ||
# physical and the ASHRAE models. | ||
|
||
# Compute IAM values using the martin_ruiz model. | ||
aoi = np.linspace(0, 90, 100) | ||
martin_ruiz_params = {'a_r': 0.16} | ||
martin_ruiz_iam = martin_ruiz(aoi, **martin_ruiz_params) | ||
|
||
# Get parameters for the physical model and compute IAM using these parameters. | ||
physical_params = convert('martin_ruiz', martin_ruiz_params, 'physical') | ||
physical_iam = physical(aoi, **physical_params) | ||
|
||
# Get parameters for the ASHRAE model and compute IAM using these parameters. | ||
ashrae_params = convert('martin_ruiz', martin_ruiz_params, 'ashrae') | ||
ashrae_iam = ashrae(aoi, **ashrae_params) | ||
|
||
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 5), sharey=True) | ||
|
||
# Plot each model's IAM vs. angle-of-incidence (AOI). | ||
ax1.plot(aoi, martin_ruiz_iam, label='Martin-Ruiz') | ||
ax1.plot(aoi, physical_iam, label='physical') | ||
ax1.set_xlabel('AOI (degrees)') | ||
ax1.set_title('Convert from Martin-Ruiz to physical') | ||
ax1.legend() | ||
|
||
ax2.plot(aoi, martin_ruiz_iam, label='Martin-Ruiz') | ||
ax2.plot(aoi, ashrae_iam, label='ASHRAE') | ||
ax2.set_xlabel('AOI (degrees)') | ||
ax2.set_title('Convert from Martin-Ruiz to ASHRAE') | ||
ax2.legend() | ||
|
||
ax1.set_ylabel('IAM') | ||
plt.show() | ||
|
||
|
||
# %% | ||
# The weight function | ||
# ------------------- | ||
# :py:func:`pvlib.iam.convert` uses a weight function when computing residuals | ||
# between the two models. The default weight | ||
# function is :math:`1 - \sin(aoi)`. We can instead pass a custom weight | ||
# function to :py:func:`pvlib.iam.convert`. | ||
# | ||
# In some cases, the choice of weight function has a minimal effect on the | ||
# returned model parameters. This is especially true when converting between | ||
# the Martin-Ruiz and physical models, because the curves described by these | ||
# models can match quite closely. However, when conversion involves the ASHRAE | ||
# model, the choice of weight function can have a meaningful effect on the | ||
# returned parameters for the target model. | ||
# | ||
# Here we'll show examples of both of these cases, starting with an example | ||
# where the choice of weight function does not have much impact. In doing | ||
# so, we'll show how to pass in a custom weight function of our choice. | ||
|
||
# Compute IAM using the Martin-Ruiz model. | ||
aoi = np.linspace(0, 90, 100) | ||
martin_ruiz_params = {'a_r': 0.16} | ||
martin_ruiz_iam = martin_ruiz(aoi, **martin_ruiz_params) | ||
|
||
# Get parameters for the physical model ... | ||
|
||
# ... using the default weight function. | ||
physical_params_default = convert('martin_ruiz', martin_ruiz_params, | ||
'physical') | ||
physical_iam_default = physical(aoi, **physical_params_default) | ||
|
||
|
||
# ... using a custom weight function. The weight function must take ``aoi`` | ||
# as its argument and return a vector of the same length as ``aoi``. | ||
def weight_function(aoi): | ||
return cosd(aoi) | ||
|
||
|
||
physical_params_custom = convert('martin_ruiz', martin_ruiz_params, 'physical', | ||
weight=weight_function) | ||
physical_iam_custom = physical(aoi, **physical_params_custom) | ||
|
||
# Plot IAM vs AOI. | ||
plt.plot(aoi, martin_ruiz_iam, label='Martin-Ruiz') | ||
plt.plot(aoi, physical_iam_default, label='Default weight function') | ||
plt.plot(aoi, physical_iam_custom, label='Custom weight function') | ||
plt.xlabel('AOI (degrees)') | ||
plt.ylabel('IAM') | ||
plt.title('Martin-Ruiz to physical') | ||
plt.legend() | ||
plt.show() | ||
|
||
# %% | ||
# For this choice of source and target models, the weight function has little | ||
# effect on the target model's parameters. | ||
# | ||
# Now we'll look at an example where the weight function does affect the | ||
# output. | ||
|
||
# Get parameters for the ASHRAE model ... | ||
|
||
# ... using the default weight function. | ||
ashrae_params_default = convert('martin_ruiz', martin_ruiz_params, 'ashrae') | ||
ashrae_iam_default = ashrae(aoi, **ashrae_params_default) | ||
|
||
# ... using the custom weight function | ||
ashrae_params_custom = convert('martin_ruiz', martin_ruiz_params, 'ashrae', | ||
weight=weight_function) | ||
ashrae_iam_custom = ashrae(aoi, **ashrae_params_custom) | ||
|
||
# Plot IAM vs AOI. | ||
plt.plot(aoi, martin_ruiz_iam, label='Martin-Ruiz') | ||
plt.plot(aoi, ashrae_iam_default, label='Default weight function') | ||
plt.plot(aoi, ashrae_iam_custom, label='Custom weight function') | ||
plt.xlabel('AOI (degrees)') | ||
plt.ylabel('IAM') | ||
plt.title('Martin-Ruiz to ASHRAE') | ||
plt.legend() | ||
plt.show() | ||
|
||
# %% | ||
# In this case, each of the two ASHRAE looks quite different. | ||
# Finding the right weight function and parameters in such cases will require | ||
# knowing where you want the target model to be more accurate. The default | ||
# weight function was chosen because it yielded IAM models that produce | ||
# similar annual insolation for a simulated PV system. | ||
|
||
# %% | ||
# Reference | ||
# --------- | ||
# .. [1] Jones, A. R., Hansen, C. W., Anderson, K. S. Parameter estimation | ||
# for incidence angle modifier models for photovoltaic modules. Sandia | ||
# report SAND2023-13944 (2023). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
|
||
""" | ||
IAM Model Fitting | ||
================================ | ||
Illustrates how to fit an IAM model to data using :py:func:`~pvlib.iam.fit`. | ||
""" | ||
|
||
# %% | ||
# An incidence angle modifier (IAM) model quantifies the fraction of direct | ||
# irradiance is that is reflected away from a module's surface. Three popular | ||
# IAM models are Martin-Ruiz :py:func:`~pvlib.iam.martin_ruiz`, physical | ||
# :py:func:`~pvlib.iam.physical`, and ASHRAE :py:func:`~pvlib.iam.ashrae`. | ||
# Each model requires one or more parameters. | ||
# | ||
# Here, we show how to use | ||
# :py:func:`~pvlib.iam.fit` to estimate a model's parameters from data. | ||
# | ||
# Model fitting require a weight function that can assign | ||
# more influence to some AOI values than others. We illustrate how to provide | ||
# a custom weight function to :py:func:`~pvlib.iam.fit`. | ||
|
||
import numpy as np | ||
from random import uniform | ||
import matplotlib.pyplot as plt | ||
|
||
from pvlib.tools import cosd | ||
from pvlib.iam import (martin_ruiz, physical, fit) | ||
|
||
|
||
# %% | ||
# Fitting an IAM model to data | ||
# ---------------------------- | ||
# | ||
# Here, we'll show how to fit an IAM model to data. | ||
# We'll generate some data by perturbing output from the Martin-Ruiz model to | ||
# mimic measured data and then we'll fit the physical model to the perturbed | ||
# data. | ||
|
||
# Create some IAM data. | ||
aoi = np.linspace(0, 85, 10) | ||
params = {'a_r': 0.16} | ||
iam = martin_ruiz(aoi, **params) | ||
data = iam * np.array([uniform(0.98, 1.02) for _ in range(len(iam))]) | ||
|
||
# Get parameters for the physical model by fitting to the perturbed data. | ||
physical_params = fit(aoi, data, 'physical') | ||
|
||
# Compute IAM with the fitted physical model parameters. | ||
physical_iam = physical(aoi, **physical_params) | ||
|
||
# Plot IAM vs. AOI | ||
plt.scatter(aoi, data, c='darkorange', label='Data') | ||
plt.plot(aoi, physical_iam, label='physical') | ||
plt.xlabel('AOI (degrees)') | ||
plt.ylabel('IAM') | ||
plt.title('Fitting the physical model to data') | ||
plt.legend() | ||
plt.show() | ||
|
||
|
||
# %% | ||
# The weight function | ||
# ------------------- | ||
# :py:func:`pvlib.iam.fit` uses a weight function when computing residuals | ||
# between the model and data. The default weight | ||
# function is :math:`1 - \sin(aoi)`. We can instead pass a custom weight | ||
# function to :py:func:`pvlib.iam.fit`. | ||
# | ||
|
||
# Define a custom weight function. The weight function must take ``aoi`` | ||
# as its argument and return a vector of the same length as ``aoi``. | ||
def weight_function(aoi): | ||
return cosd(aoi) | ||
|
||
|
||
physical_params_custom = fit(aoi, data, 'physical', weight=weight_function) | ||
|
||
physical_iam_custom = physical(aoi, **physical_params_custom) | ||
|
||
# Plot IAM vs AOI. | ||
fig, ax = plt.subplots(2, 1, figsize=(5, 8)) | ||
ax[0].plot(aoi, data, '.', label='Data (from Martin-Ruiz model)') | ||
ax[0].plot(aoi, physical_iam, label='With default weight function') | ||
ax[0].plot(aoi, physical_iam_custom, label='With custom weight function') | ||
ax[0].set_xlabel('AOI (degrees)') | ||
ax[0].set_ylabel('IAM') | ||
ax[0].legend() | ||
|
||
ax[1].plot(aoi, physical_iam_custom - physical_iam, label='Custom - default') | ||
ax[1].set_xlabel('AOI (degrees)') | ||
ax[1].set_ylabel('Diff. in IAM') | ||
ax[1].legend() | ||
plt.tight_layout() | ||
plt.show() | ||
|
||
print("Parameters with default weights: " + str(physical_params)) | ||
print("Parameters with custom weights: " + str(physical_params_custom)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,3 +17,5 @@ Incident angle modifiers | |
iam.marion_integrate | ||
iam.schlick | ||
iam.schlick_diffuse | ||
iam.convert | ||
iam.fit |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.