Skip to content

Commit

Permalink
Add functions to fit and convert IAM models (#1827)
Browse files Browse the repository at this point in the history
* 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
ajonesr and cwhanse committed Dec 18, 2023
1 parent ae84817 commit 12ba8ee
Show file tree
Hide file tree
Showing 7 changed files with 791 additions and 5 deletions.
162 changes: 162 additions & 0 deletions docs/examples/reflections/plot_convert_iam_models.py
@@ -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).
99 changes: 99 additions & 0 deletions docs/examples/reflections/plot_fit_iam_models.py
@@ -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))
2 changes: 2 additions & 0 deletions docs/sphinx/source/reference/pv_modeling/iam.rst
Expand Up @@ -17,3 +17,5 @@ Incident angle modifiers
iam.marion_integrate
iam.schlick
iam.schlick_diffuse
iam.convert
iam.fit
4 changes: 4 additions & 0 deletions docs/sphinx/source/whatsnew/v0.10.3.rst
Expand Up @@ -15,6 +15,8 @@ Enhancements
* :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`)
* Added :py:func:`~pvlib.iam.convert` and :py:func:`~pvlib.iam.fit` that
convert between IAM models, and that fit an IAM model to data. (:issue:`1824`, :pull:`1827`)

Bug fixes
~~~~~~~~~
Expand Down Expand Up @@ -50,6 +52,8 @@ Contributors
* Miguel Sánchez de León Peque (:ghuser:`Peque`)
* Will Hobbs (:ghuser:`williamhobbs`)
* Anton Driesse (:ghuser:`adriesse`)
* Abigail Jones (:ghuser:`ajonesr`)
* Cliff Hansen (:ghuser:`cwhanse`)
* Gilles Fischer (:ghuser:`GillesFischerV`)
* Adam R. Jensen (:ghusuer:`AdamRJensen`)
* :ghuser:`matsuobasho`
Expand Down

0 comments on commit 12ba8ee

Please sign in to comment.