8000 Add functions to fit and convert IAM models by ajonesr · Pull Request #1827 · pvlib/pvlib-python · GitHub
[go: up one dir, main page]

Skip to content

Add functions to fit and convert IAM models #1827

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 72 commits into from
Dec 18, 2023
Merged
Changes from 1 commit
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
8000
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 8000 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Adding functions, docstrings draft
  • Loading branch information
ajonesr committed Aug 2, 2023
commit 9816a3fb497e64cf7f40b3bb5fe86bb844b4b603
287 changes: 287 additions & 0 deletions pvlib/iam.py
8000
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import numpy as np
import pandas as pd
import functools
from scipy.optimize import minimize
from pvlib.tools import cosd, sind

# a dict of required parameter names for each IAM model
Expand Down Expand Up @@ -904,3 +905,289 @@ def schlick_diffuse(surface_tilt):
cug = pd.Series(cug, surface_tilt.index)

return cuk, cug


# ----------------------------------------------------------------


def _get_model(model_name):
# check that model is implemented
model_dict = {'ashrae': ashrae, 'martin_ruiz': martin_ruiz,
'physical': physical}
try:
model = model_dict[model_name]
except KeyError:
raise NotImplementedError(f"The {model_name} model has not been \
implemented")

return model


def _check_params(model_name, params):
# check that the parameters passed in with the model
# belong to the model
param_dict = {'ashrae': {'b'}, 'martin_ruiz': {'a_r'},
'physical': {'n', 'K', 'L'}}
expected_params = param_dict[model_name]

if set(params.keys()) != expected_params:
raise ValueError(f"The {model_name} model was expecting to be passed \
{', '.join(list(param_dict[model_name]))}, but \
was handed {', '.join(list(params.keys()))}")


def _truncated_weight(aoi, max_angle=70):
return [1 if angle <= max_angle else 0 for angle in aoi]


def _residual(aoi, source_iam, target, target_params,
weight_function=_truncated_weight,
weight_args=None):
# computes a sum of weighted differences between the source model
# and target model, using the provided weight function

if weight_args == None:
weight_args = {}

weight = weight_function(aoi, **weight_args)

# check that weight_function is behaving as expected
if np.shape(aoi) != np.shape(weight):
assert weight_function != _truncated_weight
raise ValueError('The provided custom weight function is not \
returning an object with the right shape. Please \
refer to the docstrings for a more detailed \
discussion about passing custom weight functions.')

diff = np.abs(source_iam - np.nan_to_num(target(aoi, *target_params)))
return np.sum(diff * weight)


def _ashrae_to_physical(aoi, ashrae_iam, options):
# the ashrae model has an x-intercept less than 90
# we solve for this intercept, and choose n so that the physical
# model will have the same x-intercept
int_idx = np.argwhere(ashrae_iam == 0.0).flatten()[0]
intercept = aoi[int_idx]
n = sind(intercept)

# with n fixed, we will optimize for L (recall that K and L always
# appear in the physical model as a product, so it is enough to
# optimize for just L, and to fix K=4)

# we will pass n to the optimizer to simplify things later on,
# but because we are setting (n, n) as the bounds, the optimizer
# will leave n fixed
bounds = [(0, 0.08), (n, n)]
guess = [0.002, n]

def residual_function(target_params):
L, n = target_params
return _residual(aoi, ashrae_iam, physical, [n, 4, L], **options)

return residual_function, guess, bounds


def _martin_ruiz_to_physical(aoi, martin_ruiz_iam, options):
# we will optimize for both n and L (recall that K and L always
# appear in the physical model as a product, so it is enough to
# optimize for just L, and to fix K=4)
bounds = [(0, 0.08), (1+1e-08, 2)]
guess = [0.002, 1+1e-08]

# the product of K and L is more importa 8000 nt in determining an initial
# guess for the location of the minimum, so we pass L in first
def residual_function(target_params):
L, n = target_params
return _residual(aoi, martin_ruiz_iam, physical, [n, 4, L], **options)

return residual_function, guess, bounds


def _minimize(residual_function, guess, bounds):
optimize_result = minimize(residual_function, guess, method="powell",
bounds=bounds)

if not optimize_result.success:
try:
message = "Optimizer exited unsuccessfully:" \
+ optimize_result.message
except AttributeError:
message = "Optimizer exited unsuccessfully: \
No message explaining the failure was returned. \
If you would like to see this message, please \
update your scipy version (try version 1.8.0 \
or beyond)."
raise RuntimeError(message)

return optimize_result


def _process_return(target_name, optimize_result):
if target_name == "ashrae":
target_params = {'b': optimize_result.x}

elif target_name == "martin_ruiz":
target_params = {'a_r': optimize_result.x}

elif target_name == "physical":
L, n = optimize_result.x
target_params = {'n': n, 'K': 4, 'L': L}

return target_params


def convert(source_name, source_params, target_name, options=None):
"""
Given a source model and its parameters, determines the best
parameters for the target model so that the models behave
similarly. (FIXME)

Parameters
----------
source_name : str
Name of source model. Must be 'ashrae', 'martin_ruiz', or
'physical'.

source_params : dict
A dictionary of parameters for the source model. See table
below to get keys needed for each model. (Note that the keys
for the physical model are case-sensitive!)

+--------------+----------+
| source model | keys |
+==============+==========+
| ashrae | b |
+--------------+----------+
| martin_ruiz | a_r |
+--------------+----------+
| physical | n, K, L |
+--------------+----------+

target_name : str
Name of target model. Must be 'ashrae', 'martin_ruiz', or
'physical'.

options : dict, optional
A dictionary that allows passing a custom weight function and
arguments to the (default or custom) weight function. Possible
keys are 'weight_function' and 'weight_args'

weight_function : function
A function that outputs an array of weights to use
when computing residuals between models.

Requirements:
-------------
1. Must accept aoi as first argument. (aoi is a numpy
array, and it is handed to the function internally.)
2. Any other arguments must be keyword arguments. (These
will be passed by the user in weight_args, see below.)
3. Must return an array-like object with the same shape
as aoi.

weight_args : dict
A dictionary containing all keyword arguments for the
weight function. If using the default weight function,
the only keyword argument is max_angle.

FIXME there needs to be more information about the default
weight function, so people don't have to go digging through the
private functions.

Default value of options is None (leaving as default will use
default weight function `pvlib.iam._truncated_weight`).
* FIXME if name of default function changes *

Returns
-------
dict
Parameters for target model that best match the behavior of the
given source model. Key names are given in the table below.
(Note that the keys for the physical model are case-sensitive!)

+--------------+----------+
| target model | keys |
+==============+==========+
| ashrae | b |
+--------------+----------+
| martin_ruiz | a_r |
+--------------+----------+
| physical | n, K, L |
+--------------+----------+

References
----------
.. [1] TODO

See Also
--------
pvlib.iam.fit
pvlib.iam.ashrae
pvlib.iam.martin_ruiz
pvlib.iam.physical
"""

source = _get_model(source_name)
target = _get_model(target_name)

# if no options were passed in, we will use the default arguments
if options == None:
options = {}

aoi = np.linspace(0, 90, 100)
_check_params(source_name, source_params)
source_iam = source(aoi, **source_params)

if target_name == "physical":
# we can do some special set-up to improve the fit when the
# target model is physical
if source_name == "ashrae":
residual_function, guess, bounds = \
_ashrae_to_physical(aoi, source_iam, options)
elif source_name == "martin_ruiz":
residual_function, guess, bounds = \
_martin_ruiz_to_physical(aoi, source_iam, options)

else:
# otherwise, target model is ashrae or martin_ruiz, and scipy
# does fine without any special set-up
bounds = [(0, 1)]
guess = [1e-08]
def residual_function(target_param):
return _residual(aoi, source_iam, target, target_param, **options)

optimize_result = _minimize(residual_function, guess, bounds)

return _process_return(target_name, optimize_result)


def fit(measured_aoi 7623 , measured_iam, target_name, options=None):
# given measured aoi and iam data and a target model, finds
# parameters for target model that best fit measured data
target = _get_model(target_name)

# if no options were passed in, we will use the default arguments
if options == None:
options = {}

if target_name == "physical":
bounds = [(0, 0.08), (1+1e-08, 2)]
guess = [0.002, 1+1e-08]

def residual_function(target_params):
L, n = target_params
return _residual(measured_aoi, measured_iam, target, [n, 4, L],
**options)

else: # target_name == martin_ruiz or target_name == ashrae
bounds = [(0, 1)]
guess = [1e-08]
def residual_function(target_param):
return _residual(measured_aoi, measured_iam, target, target_param,
**options)

optimize_result = _minimize(residual_function, guess, bounds)

return _process_return(target_name, optimize_result)

0