Skip to content
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

Add functions to fit and convert IAM models #1827

Merged
merged 72 commits into from Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from 59 commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
9816a3f
Adding functions, docstrings draft
ajonesr Aug 2, 2023
dacc1f0
Updating imports, adding docstrings
ajonesr Aug 2, 2023
fc968af
Updated rst file
ajonesr Aug 2, 2023
441d8cc
Make linter edits
ajonesr Aug 3, 2023
77641d0
Docstring experiment
ajonesr Aug 4, 2023
d7cbf5e
Added tests
ajonesr Aug 4, 2023
b4dcecf
More linter edits
ajonesr Aug 4, 2023
6d054f4
Docstrings and linter edits
ajonesr Aug 8, 2023
21a66ee
Docstrings and linter
ajonesr Aug 8, 2023
b4714a4
LINTER
ajonesr Aug 8, 2023
96de7d9
Docstrings edit
ajonesr Aug 8, 2023
de20629
Added more tests
ajonesr Aug 8, 2023
91f811e
Annihilate spaces
ajonesr Aug 8, 2023
e4d4cdc
Spacing
ajonesr Aug 11, 2023
3789890
Changed default weight function
ajonesr Aug 22, 2023
2efa830
Silence numpy warning
ajonesr Aug 22, 2023
c5c2d09
Updating tests to work with new default
ajonesr Aug 22, 2023
af65bc0
Forgot a comment
ajonesr Aug 22, 2023
d505ac9
Return dict contains scalars now, instead of arrays
ajonesr Aug 22, 2023
109e20e
Adding option to not fix n
ajonesr Aug 22, 2023
554d862
Adding straggler tests
ajonesr Aug 22, 2023
e8d83b6
Removing examples specific to old default weight function
ajonesr Aug 22, 2023
ee9c686
Linter nitpicks
ajonesr Aug 22, 2023
e95993d
Update docstrings
ajonesr Aug 22, 2023
991e962
Experimenting with example
ajonesr Aug 22, 2023
484cb5a
Adjusting figure size
ajonesr Aug 22, 2023
47ebdac
Edit gallery example
ajonesr Aug 23, 2023
317fb35
Fixing bounds
ajonesr Aug 23, 2023
3996cab
Linter
ajonesr Aug 23, 2023
ba87f7e
Example experimentation
ajonesr Aug 23, 2023
ac4e717
Merge branch 'main' of https://github.com/pvlib/pvlib-python into con…
cwhanse Sep 1, 2023
529e512
exact ashrae intercept
cwhanse Sep 1, 2023
6b211fd
Merge branch 'main' of https://github.com/pvlib/pvlib-python into con…
cwhanse Sep 11, 2023
3fc2c00
editing docstrings mostly
cwhanse Sep 12, 2023
a9f9b74
whatsnew
cwhanse Sep 12, 2023
ac160b8
fix errors
cwhanse Sep 12, 2023
536cb9f
remove test for weight function size
cwhanse Sep 12, 2023
2882912
editing
cwhanse Sep 12, 2023
9bb36b5
simplify weight function
cwhanse Sep 12, 2023
753d72b
Merge branch 'main' of https://github.com/pvlib/pvlib-python into con…
cwhanse Oct 16, 2023
fc1316c
improve martin_ruiz to physical, generalize tests
cwhanse Oct 16, 2023
935443b
fix examples, split convert and fit examples
cwhanse Oct 16, 2023
c9f697d
linter, improve coverage
cwhanse Oct 16, 2023
88a9dfc
spacing
cwhanse Oct 16, 2023
8ace9d6
fix reverse order test
cwhanse Oct 16, 2023
3475bf4
improve examples
cwhanse Oct 17, 2023
f216d94
print parameters
cwhanse Oct 17, 2023
cb4cb05
whatsnew
cwhanse Oct 17, 2023
ed35731
remove v0.10.2 whatsnew
cwhanse Oct 17, 2023
fdcc952
Revert "remove v0.10.2 whatsnew"
cwhanse Oct 17, 2023
9b1cfd8
put v0.10.2.rst right again
cwhanse Oct 17, 2023
d78265a
Merge branch 'main' of https://github.com/pvlib/pvlib-python into con…
cwhanse Oct 17, 2023
38bfb58
require scipy>=1.5.0
cwhanse Oct 17, 2023
04121de
linter
cwhanse Oct 17, 2023
69cd00a
linter
cwhanse Oct 17, 2023
520a74e
Merge branch 'main' of https://github.com/pvlib/pvlib-python into con…
cwhanse Nov 28, 2023
e5cd24b
suggestions from review
cwhanse Nov 28, 2023
6ce34e4
add reference
cwhanse Nov 28, 2023
743931d
edits to examples
cwhanse Nov 28, 2023
fe9a39c
add note to convert
cwhanse Nov 28, 2023
d56cbcf
edit note on convert
cwhanse Nov 28, 2023
2a0b815
edit both notes
cwhanse Nov 28, 2023
4e165a0
polish the notes
cwhanse Nov 28, 2023
d3d8cfd
sum not Sum
cwhanse Nov 28, 2023
313386c
edits
cwhanse Nov 29, 2023
b0e45dd
Merge branch 'main' of https://github.com/pvlib/pvlib-python into con…
cwhanse Nov 29, 2023
32aa64a
remove test for scipy
cwhanse Nov 29, 2023
8df7bf6
edits from review
cwhanse Nov 29, 2023
6250182
its not it's
cwhanse Nov 30, 2023
74c2e54
Merge branch 'main' of https://github.com/pvlib/pvlib-python into con…
cwhanse Dec 18, 2023
f9c8888
change internal linspace to one degree intervals
cwhanse Dec 18, 2023
79af432
use linspace(0, 90, 91)
cwhanse Dec 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
161 changes: 161 additions & 0 deletions docs/examples/reflections/plot_convert_iam_models.py
@@ -0,0 +1,161 @@

"""
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.
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).
98 changes: 98 additions & 0 deletions docs/examples/reflections/plot_fit_iam_models.py
@@ -0,0 +1,98 @@

"""
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 and perturb IAM data.
aoi = np.linspace(0, 90, 100)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
aoi = np.linspace(0, 90, 100)
aoi = np.linspace(0, 85, 18)

This would more representative for measurements.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest you say a bit more about the shape and purpose of these functions. I see there is more in the other example, but I think it is more relevant or important here.

# 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.
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 @@ -12,6 +12,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 All @@ -34,6 +36,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`)
* :ghuser:`matsuobasho`
* Harry Jack (:ghuser:`harry-solcast`)
* Adam R. Jensen (:ghuser:`AdamRJensen`)
Expand Down