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
Show file tree
Hide file tree
Changes from 2 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/examples/reflections/plot_convert_iam_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@
physical_iam_default = physical(aoi, **physical_params_default)


# ... using a custom weight function.
# ... using a custom weight function. The weight function must take ``aoi``
# as it's argument and return a vector of the same length as ``aoi``.
def weight_function(aoi):
return cosd(aoi)

Expand Down
7 changes: 4 additions & 3 deletions docs/examples/reflections/plot_fit_iam_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
# 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)
# 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))])
Expand Down Expand Up @@ -69,7 +69,8 @@
# function to :py:func:`pvlib.iam.fit`.
#

# Define a custom weight function.
# Define a custom weight function. The weight function must take ``aoi``
# as it's argument and return a vector of the same length as ``aoi``.
def weight_function(aoi):
return cosd(aoi)

Expand Down
112 changes: 45 additions & 67 deletions pvlib/iam.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import numpy as np
import pandas as pd
import functools
import warnings
from scipy.optimize import minimize
from pvlib.tools import cosd, sind, acosd

Expand Down Expand Up @@ -1094,18 +1093,6 @@ def _process_return(target_name, optimize_result):
return target_params


def _min_scipy():
'''convert and fit require scipy>=1.5.0
'''
from scipy import __version__
major, minor = __version__.split('.')[:2]
if (int(major) >= 1) & (int(minor) >= 5):
return True
else:
warnings.warn('iam.convert and iam.fit require scipy>=1.5.0')
return False


def convert(source_name, source_params, target_name, weight=_sin_weight,
fix_n=True, xtol=None):
"""
Expand Down Expand Up @@ -1192,43 +1179,38 @@ def convert(source_name, source_params, target_name, weight=_sin_weight,
pvlib.iam.martin_ruiz
pvlib.iam.physical
"""
if _min_scipy():

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

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, weight, fix_n,
source_params['b'])
elif source_name == "martin_ruiz":
residual_function, guess, bounds = \
_martin_ruiz_to_physical(aoi, source_iam, weight,
source_params['a_r'])
source = _get_model(source_name)
target = _get_model(target_name)

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, weight, fix_n,
source_params['b'])
elif source_name == "martin_ruiz":
residual_function, guess, bounds = \
_martin_ruiz_to_physical(aoi, source_iam, weight,
source_params['a_r'])

else:
# otherwise, target model is ashrae or martin_ruiz, and scipy
# does fine without any special set-up
bounds = [(1e-04, 1)]
guess = [1e-03]
else:
# otherwise, target model is ashrae or martin_ruiz, and scipy
# does fine without any special set-up
bounds = [(1e-04, 1)]
guess = [1e-03]

def residual_function(target_param):
return _residual(aoi, source_iam, target, target_param, weight)
def residual_function(target_param):
return _residual(aoi, source_iam, target, target_param, weight)

optimize_result = _minimize(residual_function, guess, bounds,
xtol=xtol)
optimize_result = _minimize(residual_function, guess, bounds,
xtol=xtol)

return _process_return(target_name, optimize_result)

else:
return {}
return _process_return(target_name, optimize_result)


def fit(measured_aoi, measured_iam, model_name, weight=_sin_weight, xtol=None):
Expand Down Expand Up @@ -1295,30 +1277,26 @@ def fit(measured_aoi, measured_iam, model_name, weight=_sin_weight, xtol=None):
pvlib.iam.martin_ruiz
pvlib.iam.physical
"""
if _min_scipy():

target = _get_model(model_name)
target = _get_model(model_name)

if model_name == "physical":
bounds = [(0, 0.08), (1, 2)]
guess = [0.002, 1+1e-08]
if model_name == "physical":
bounds = [(0, 0.08), (1, 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],
weight)
def residual_function(target_params):
L, n = target_params
return _residual(measured_aoi, measured_iam, target, [n, 4, L],
weight)

# otherwise, target_name is martin_ruiz or ashrae
else:
bounds = [(1e-08, 1)]
guess = [0.05]
# otherwise, target_name is martin_ruiz or ashrae
else:
bounds = [(1e-08, 1)]
guess = [0.05]

def residual_function(target_param):
return _residual(measured_aoi, measured_iam, target,
target_param, weight)
def residual_function(target_param):
return _residual(measured_aoi, measured_iam, target,
target_param, weight)

optimize_result = _minimize(residual_function, guess, bounds, xtol)
optimize_result = _minimize(residual_function, guess, bounds, xtol)

return _process_return(model_name, optimize_result)
else:
return {}
return _process_return(model_name, optimize_result)
8 changes: 1 addition & 7 deletions pvlib/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import pvlib
from pvlib.location import Location
from pvlib.iam import _min_scipy


pvlib_base_version = Version(Version(pvlib.__version__).base_version)

Expand Down Expand Up @@ -161,12 +161,6 @@ def has_numba():
requires_pysam = pytest.mark.skipif(not has_pysam, reason="requires PySAM")


iam_scipy_ok = _min_scipy()

requires_scipy_150 = pytest.mark.skipif(not iam_scipy_ok,
reason="requires scipy>=1.5.0")


@pytest.fixture()
def golden():
return Location(39.742476, -105.1786, 'America/Denver', 1830.14)
Expand Down
16 changes: 1 addition & 15 deletions pvlib/tests/test_iam.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import pandas as pd

import pytest
from .conftest import assert_series_equal, requires_scipy_150
from .conftest import assert_series_equal
from numpy.testing import assert_allclose

from pvlib import iam as _iam
Expand Down Expand Up @@ -397,7 +397,6 @@ def test_schlick_diffuse():
rtol=1e-6)


@requires_scipy_150
@pytest.mark.parametrize('source,source_params,target,expected', [
('physical', {'n': 1.5, 'K': 4.5, 'L': 0.004}, 'martin_ruiz',
{'a_r': 0.173972}),
Expand All @@ -416,7 +415,6 @@ def test_convert(source, source_params, target, expected):
assert_allclose(exp, tar, rtol=1e-05)


@requires_scipy_150
@pytest.mark.parametrize('source,source_params', [
('ashrae', {'b': 0.15}),
('ashrae', {'b': 0.05}),
Expand All @@ -429,7 +427,6 @@ def test_convert_recover(source, source_params):
assert_allclose(exp, tar, rtol=1e-05)


@requires_scipy_150
def test_convert_ashrae_physical_no_fix_n():
# convert ashrae to physical, without fixing n
source_params = {'b': 0.15}
Expand All @@ -441,7 +438,6 @@ def test_convert_ashrae_physical_no_fix_n():
assert_allclose(exp, tar, rtol=1e-05)


@requires_scipy_150
def test_convert_reverse_order_in_physical():
source_params = {'a_r': 0.25}
target_params = _iam.convert('martin_ruiz', source_params, 'physical')
Expand All @@ -451,7 +447,6 @@ def test_convert_reverse_order_in_physical():
assert_allclose(exp, tar, rtol=1e-5)


@requires_scipy_150
def test_convert_xtol():
source_params = {'b': 0.15}
target_params = _iam.convert('ashrae', source_params, 'physical',
Expand All @@ -462,7 +457,6 @@ def test_convert_xtol():
assert_allclose(exp, tar, rtol=1e-10)


@requires_scipy_150
def test_convert_custom_weight_func():
aoi = np.linspace(0, 90, 100)

Expand All @@ -486,19 +480,16 @@ def scaled_weight(aoi):
assert np.isclose(expected_min_res, actual_min_res, atol=1e-08)


@requires_scipy_150
def test_convert_model_not_implemented():
with pytest.raises(NotImplementedError, match='model has not been'):
_iam.convert('ashrae', {'b': 0.1}, 'foo')


@requires_scipy_150
def test_convert_wrong_model_parameters():
with pytest.raises(ValueError, match='model was expecting'):
_iam.convert('ashrae', {'B': 0.1}, 'physical')


@requires_scipy_150
def test_convert__minimize_fails():
# to make scipy.optimize.minimize fail, we'll pass in a nonsense
# weight function that only outputs nans
Expand All @@ -509,7 +500,6 @@ def nan_weight(aoi):
_iam.convert('ashrae', {'b': 0.1}, 'physical', weight=nan_weight)


@requires_scipy_150
def test_fit():
aoi = np.linspace(0, 90, 5)
perturb = np.array([1.2, 1.01, 0.95, 1, 0.98])
Expand All @@ -523,7 +513,6 @@ def test_fit():
assert np.isclose(expected_a_r, actual_a_r, atol=1e-04)


@requires_scipy_150
def test_fit_custom_weight_func():
# define custom weight function that takes in other arguments
def scaled_weight(aoi):
Expand All @@ -542,13 +531,11 @@ def scaled_weight(aoi):
assert np.isclose(expected_a_r, actual_a_r, atol=1e-04)


@requires_scipy_150
def test_fit_model_not_implemented():
with pytest.raises(NotImplementedError, match='model has not been'):
_iam.fit(np.array([0, 10]), np.array([1, 0.99]), 'foo')


@requires_scipy_150
def test_fit__minimize_fails():
# to make scipy.optimize.minimize fail, we'll pass in a nonsense
# weight function that only outputs nans
Expand All @@ -560,7 +547,6 @@ def nan_weight(aoi):
weight=nan_weight)


@requires_scipy_150
def test__residual_zero_outside_range():
# check that _residual annihilates any weights that come from aoi
# outside of interval [0, 90] (this is important for `iam.fit`, when
Expand Down