Skip to content

Commit 12ba8ee

Browse files
ajonesrcwhanse
andauthored
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 <[email protected]>
1 parent ae84817 commit 12ba8ee

File tree

7 files changed

+791
-5
lines changed

7 files changed

+791
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
2+
"""
3+
IAM Model Conversion
4+
====================
5+
6+
Illustrates how to convert from one IAM model to a different model using
7+
:py:func:`~pvlib.iam.convert`.
8+
9+
"""
10+
11+
# %%
12+
# An incidence angle modifier (IAM) model quantifies the fraction of direct
13+
# irradiance that is reflected away from a module's surface. Three popular
14+
# IAM models are Martin-Ruiz :py:func:`~pvlib.iam.martin_ruiz`, physical
15+
# :py:func:`~pvlib.iam.physical`, and ASHRAE :py:func:`~pvlib.iam.ashrae`.
16+
# Each model requires one or more parameters.
17+
#
18+
# Here, we show how to use
19+
# :py:func:`~pvlib.iam.convert` to estimate parameters for a desired target
20+
# IAM model from a source IAM model. Model conversion uses a weight
21+
# function that can assign more influence to some AOI values than others.
22+
# We illustrate how to provide a custom weight function to
23+
# :py:func:`~pvlib.iam.convert`.
24+
25+
import numpy as np
26+
import matplotlib.pyplot as plt
27+
28+
from pvlib.tools import cosd
29+
from pvlib.iam import (ashrae, martin_ruiz, physical, convert)
30+
31+
# %%
32+
# Converting from one IAM model to another model
33+
# ----------------------------------------------
34+
#
35+
# Here we'll show how to convert from the Martin-Ruiz model to the
36+
# physical and the ASHRAE models.
37+
38+
# Compute IAM values using the martin_ruiz model.
39+
aoi = np.linspace(0, 90, 100)
40+
martin_ruiz_params = {'a_r': 0.16}
41+
martin_ruiz_iam = martin_ruiz(aoi, **martin_ruiz_params)
42+
43+
# Get parameters for the physical model and compute IAM using these parameters.
44+
physical_params = convert('martin_ruiz', martin_ruiz_params, 'physical')
45+
physical_iam = physical(aoi, **physical_params)
46+
47+
# Get parameters for the ASHRAE model and compute IAM using these parameters.
48+
ashrae_params = convert('martin_ruiz', martin_ruiz_params, 'ashrae')
49+
ashrae_iam = ashrae(aoi, **ashrae_params)
50+
51+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 5), sharey=True)
52+
53+
# Plot each model's IAM vs. angle-of-incidence (AOI).
54+
ax1.plot(aoi, martin_ruiz_iam, label='Martin-Ruiz')
55+
ax1.plot(aoi, physical_iam, label='physical')
56+
ax1.set_xlabel('AOI (degrees)')
57+
ax1.set_title('Convert from Martin-Ruiz to physical')
58+
ax1.legend()
59+
60+
ax2.plot(aoi, martin_ruiz_iam, label='Martin-Ruiz')
61+
ax2.plot(aoi, ashrae_iam, label='ASHRAE')
62+
ax2.set_xlabel('AOI (degrees)')
63+
ax2.set_title('Convert from Martin-Ruiz to ASHRAE')
64+
ax2.legend()
65+
66+
ax1.set_ylabel('IAM')
67+
plt.show()
68+
69+
70+
# %%
71+
# The weight function
72+
# -------------------
73+
# :py:func:`pvlib.iam.convert` uses a weight function when computing residuals
74+
# between the two models. The default weight
75+
# function is :math:`1 - \sin(aoi)`. We can instead pass a custom weight
76+
# function to :py:func:`pvlib.iam.convert`.
77+
#
78+
# In some cases, the choice of weight function has a minimal effect on the
79+
# returned model parameters. This is especially true when converting between
80+
# the Martin-Ruiz and physical models, because the curves described by these
81+
# models can match quite closely. However, when conversion involves the ASHRAE
82+
# model, the choice of weight function can have a meaningful effect on the
83+
# returned parameters for the target model.
84+
#
85+
# Here we'll show examples of both of these cases, starting with an example
86+
# where the choice of weight function does not have much impact. In doing
87+
# so, we'll show how to pass in a custom weight function of our choice.
88+
89+
# Compute IAM using the Martin-Ruiz model.
90+
aoi = np.linspace(0, 90, 100)
91+
martin_ruiz_params = {'a_r': 0.16}
92+
martin_ruiz_iam = martin_ruiz(aoi, **martin_ruiz_params)
93+
94+
# Get parameters for the physical model ...
95+
96+
# ... using the default weight function.
97+
physical_params_default = convert('martin_ruiz', martin_ruiz_params,
98+
'physical')
99+
physical_iam_default = physical(aoi, **physical_params_default)
100+
101+
102+
# ... using a custom weight function. The weight function must take ``aoi``
103+
# as its argument and return a vector of the same length as ``aoi``.
104+
def weight_function(aoi):
105+
return cosd(aoi)
106+
107+
108+
physical_params_custom = convert('martin_ruiz', martin_ruiz_params, 'physical',
109+
weight=weight_function)
110+
physical_iam_custom = physical(aoi, **physical_params_custom)
111+
112+
# Plot IAM vs AOI.
113+
plt.plot(aoi, martin_ruiz_iam, label='Martin-Ruiz')
114+
plt.plot(aoi, physical_iam_default, label='Default weight function')
115+
plt.plot(aoi, physical_iam_custom, label='Custom weight function')
116+
plt.xlabel('AOI (degrees)')
117+
plt.ylabel('IAM')
118+
plt.title('Martin-Ruiz to physical')
119+
plt.legend()
120+
plt.show()
121+
122+
# %%
123+
# For this choice of source and target models, the weight function has little
124+
# effect on the target model's parameters.
125+
#
126+
# Now we'll look at an example where the weight function does affect the
127+
# output.
128+
129+
# Get parameters for the ASHRAE model ...
130+
131+
# ... using the default weight function.
132+
ashrae_params_default = convert('martin_ruiz', martin_ruiz_params, 'ashrae')
133+
ashrae_iam_default = ashrae(aoi, **ashrae_params_default)
134+
135+
# ... using the custom weight function
136+
ashrae_params_custom = convert('martin_ruiz', martin_ruiz_params, 'ashrae',
137+
weight=weight_function)
138+
ashrae_iam_custom = ashrae(aoi, **ashrae_params_custom)
139+
140+
# Plot IAM vs AOI.
141+
plt.plot(aoi, martin_ruiz_iam, label='Martin-Ruiz')
142+
plt.plot(aoi, ashrae_iam_default, label='Default weight function')
143+
plt.plot(aoi, ashrae_iam_custom, label='Custom weight function')
144+
plt.xlabel('AOI (degrees)')
145+
plt.ylabel('IAM')
146+
plt.title('Martin-Ruiz to ASHRAE')
147+
plt.legend()
148+
plt.show()
149+
150+
# %%
151+
# In this case, each of the two ASHRAE looks quite different.
152+
# Finding the right weight function and parameters in such cases will require
153+
# knowing where you want the target model to be more accurate. The default
154+
# weight function was chosen because it yielded IAM models that produce
155+
# similar annual insolation for a simulated PV system.
156+
157+
# %%
158+
# Reference
159+
# ---------
160+
# .. [1] Jones, A. R., Hansen, C. W., Anderson, K. S. Parameter estimation
161+
# for incidence angle modifier models for photovoltaic modules. Sandia
162+
# report SAND2023-13944 (2023).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
2+
"""
3+
IAM Model Fitting
4+
================================
5+
6+
Illustrates how to fit an IAM model to data using :py:func:`~pvlib.iam.fit`.
7+
8+
"""
9+
10+
# %%
11+
# An incidence angle modifier (IAM) model quantifies the fraction of direct
12+
# irradiance is that is reflected away from a module's surface. Three popular
13+
# IAM models are Martin-Ruiz :py:func:`~pvlib.iam.martin_ruiz`, physical
14+
# :py:func:`~pvlib.iam.physical`, and ASHRAE :py:func:`~pvlib.iam.ashrae`.
15+
# Each model requires one or more parameters.
16+
#
17+
# Here, we show how to use
18+
# :py:func:`~pvlib.iam.fit` to estimate a model's parameters from data.
19+
#
20+
# Model fitting require a weight function that can assign
21+
# more influence to some AOI values than others. We illustrate how to provide
22+
# a custom weight function to :py:func:`~pvlib.iam.fit`.
23+
24+
import numpy as np
25+
from random import uniform
26+
import matplotlib.pyplot as plt
27+
28+
from pvlib.tools import cosd
29+
from pvlib.iam import (martin_ruiz, physical, fit)
30+
31+
32+
# %%
33+
# Fitting an IAM model to data
34+
# ----------------------------
35+
#
36+
# Here, we'll show how to fit an IAM model to data.
37+
# We'll generate some data by perturbing output from the Martin-Ruiz model to
38+
# mimic measured data and then we'll fit the physical model to the perturbed
39+
# data.
40+
41+
# Create some IAM data.
42+
aoi = np.linspace(0, 85, 10)
43+
params = {'a_r': 0.16}
44+
iam = martin_ruiz(aoi, **params)
45+
data = iam * np.array([uniform(0.98, 1.02) for _ in range(len(iam))])
46+
47+
# Get parameters for the physical model by fitting to the perturbed data.
48+
physical_params = fit(aoi, data, 'physical')
49+
50+
# Compute IAM with the fitted physical model parameters.
51+
physical_iam = physical(aoi, **physical_params)
52+
53+
# Plot IAM vs. AOI
54+
plt.scatter(aoi, data, c='darkorange', label='Data')
55+
plt.plot(aoi, physical_iam, label='physical')
56+
plt.xlabel('AOI (degrees)')
57+
plt.ylabel('IAM')
58+
plt.title('Fitting the physical model to data')
59+
plt.legend()
60+
plt.show()
61+
62+
63+
# %%
64+
# The weight function
65+
# -------------------
66+
# :py:func:`pvlib.iam.fit` uses a weight function when computing residuals
67+
# between the model and data. The default weight
68+
# function is :math:`1 - \sin(aoi)`. We can instead pass a custom weight
69+
# function to :py:func:`pvlib.iam.fit`.
70+
#
71+
72+
# Define a custom weight function. The weight function must take ``aoi``
73+
# as its argument and return a vector of the same length as ``aoi``.
74+
def weight_function(aoi):
75+
return cosd(aoi)
76+
77+
78+
physical_params_custom = fit(aoi, data, 'physical', weight=weight_function)
79+
80+
physical_iam_custom = physical(aoi, **physical_params_custom)
81+
82+
# Plot IAM vs AOI.
83+
fig, ax = plt.subplots(2, 1, figsize=(5, 8))
84+
ax[0].plot(aoi, data, '.', label='Data (from Martin-Ruiz model)')
85+
ax[0].plot(aoi, physical_iam, label='With default weight function')
86+
ax[0].plot(aoi, physical_iam_custom, label='With custom weight function')
87+
ax[0].set_xlabel('AOI (degrees)')
88+
ax[0].set_ylabel('IAM')
89+
ax[0].legend()
90+
91+
ax[1].plot(aoi, physical_iam_custom - physical_iam, label='Custom - default')
92+
ax[1].set_xlabel('AOI (degrees)')
93+
ax[1].set_ylabel('Diff. in IAM')
94+
ax[1].legend()
95+
plt.tight_layout()
96+
plt.show()
97+
98+
print("Parameters with default weights: " + str(physical_params))
99+
print("Parameters with custom weights: " + str(physical_params_custom))

docs/sphinx/source/reference/pv_modeling/iam.rst

+2
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ Incident angle modifiers
1717
iam.marion_integrate
1818
iam.schlick
1919
iam.schlick_diffuse
20+
iam.convert
21+
iam.fit

docs/sphinx/source/whatsnew/v0.10.3.rst

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Enhancements
1515
* :py:func:`pvlib.bifacial.infinite_sheds.get_irradiance` and
1616
:py:func:`pvlib.bifacial.infinite_sheds.get_irradiance_poa` now include
1717
shaded fraction in returned variables. (:pull:`1871`)
18+
* Added :py:func:`~pvlib.iam.convert` and :py:func:`~pvlib.iam.fit` that
19+
convert between IAM models, and that fit an IAM model to data. (:issue:`1824`, :pull:`1827`)
1820

1921
Bug fixes
2022
~~~~~~~~~
@@ -50,6 +52,8 @@ Contributors
5052
* Miguel Sánchez de León Peque (:ghuser:`Peque`)
5153
* Will Hobbs (:ghuser:`williamhobbs`)
5254
* Anton Driesse (:ghuser:`adriesse`)
55+
* Abigail Jones (:ghuser:`ajonesr`)
56+
* Cliff Hansen (:ghuser:`cwhanse`)
5357
* Gilles Fischer (:ghuser:`GillesFischerV`)
5458
* Adam R. Jensen (:ghusuer:`AdamRJensen`)
5559
* :ghuser:`matsuobasho`

0 commit comments

Comments
 (0)