Skip to content

Commit f8b1294

Browse files
Make interp IAM method available for modelchain (pvlib#1832)
* Make interp method available for modelchain * Update test_pvsystem.py * Update v0.10.2.rst * Apply Kevin's suggestions Co-Authored-By: Kevin Anderson <[email protected]> * Update test_modelchain.py * Update error message in modelchain.py Co-Authored-By: Kevin Anderson <[email protected]> * Allow non _IAM_MODEL_PARAMS to be passed to IAM models in pvsystem * Add kwargs test * Update v0.10.2.rst * Update pvlib/modelchain.py Co-authored-by: Kevin Anderson <[email protected]> * Update modelchain.py --------- Co-authored-by: Kevin Anderson <[email protected]> Co-authored-by: Kevin Anderson <[email protected]>
1 parent 80edabe commit f8b1294

File tree

6 files changed

+92
-28
lines changed

6 files changed

+92
-28
lines changed

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,17 @@ Enhancements
1919
:py:func:`pvlib.clearsky.detect_clearsky` (:issue:`1808`, :pull:`1784`)
2020
* Added a continuous version of the Erbs diffuse-fraction/decomposition model.
2121
:py:func:`pvlib.irradiance.erbs_driesse` (:issue:`1755`, :pull:`1834`)
22-
22+
* Added :py:func:`~pvlib.iam.interp` option as AOI losses model in
23+
:py:class:`pvlib.modelchain.ModelChain` and
24+
:py:class:`pvlib.pvsystem.PVSystem`. (:issue:`1742`, :pull:`1832`)
2325

2426
Bug fixes
2527
~~~~~~~~~
2628
* :py:func:`~pvlib.iotools.get_psm3` no longer incorrectly returns clear-sky
2729
DHI instead of clear-sky GHI when requesting ``ghi_clear``. (:pull:`1819`)
30+
* :py:class:`pvlib.pvsystem.PVSystem` now correctly passes ``n_ar`` module
31+
parameter to :py:func:`pvlib.iam.physical` when this IAM model is specified
32+
or inferred. (:pull:`1832`)
2833

2934
Testing
3035
~~~~~~~
@@ -53,6 +58,8 @@ Contributors
5358
* Adam R. Jensen (:ghuser:`AdamRJensen`)
5459
* Abigail Jones (:ghuser:`ajonesr`)
5560
* Taos Transue (:ghuser:`reepoi`)
61+
* Echedey Luis (:ghuser:`echedey-ls`)
62+
* Todd Karin (:ghuser:`toddkarin`)
5663
* NativeSci (:ghuser:`nativesci`)
5764
* Anton Driesse (:ghuser:`adriesse`)
5865
* Lukas Grossar (:ghuser:`tongpu`)

pvlib/iam.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
'physical': {'n', 'K', 'L'},
2121
'martin_ruiz': {'a_r'},
2222
'sapm': {'B0', 'B1', 'B2', 'B3', 'B4', 'B5'},
23-
'interp': set()
23+
'interp': {'theta_ref', 'iam_ref'}
2424
}
2525

2626

pvlib/modelchain.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,9 @@
1414
from typing import Union, Tuple, Optional, TypeVar
1515

1616
from pvlib import (atmosphere, clearsky, inverter, pvsystem, solarposition,
17-
temperature)
17+
temperature, iam)
1818
import pvlib.irradiance # avoid name conflict with full import
1919
from pvlib.pvsystem import _DC_MODEL_PARAMS
20-
from pvlib._deprecation import pvlibDeprecationWarning
2120
from pvlib.tools import _build_kwargs
2221

2322
from pvlib._deprecation import deprecated
@@ -279,7 +278,7 @@ def _mcr_repr(obj):
279278
# scalar, None, other?
280279
return repr(obj)
281280

282-
281+
283282
# Type for fields that vary between arrays
284283
T = TypeVar('T')
285284

@@ -490,7 +489,7 @@ class ModelChain:
490489
If None, the model will be inferred from the parameters that
491490
are common to all of system.arrays[i].module_parameters.
492491
Valid strings are 'physical', 'ashrae', 'sapm', 'martin_ruiz',
493-
'no_loss'. The ModelChain instance will be passed as the
492+
'interp' and 'no_loss'. The ModelChain instance will be passed as the
494493
first argument to a user-defined function.
495494
496495
spectral_model: None, str, or function, default None
@@ -917,6 +916,8 @@ def aoi_model(self, model):
917916
self._aoi_model = self.sapm_aoi_loss
918917
elif model == 'martin_ruiz':
919918
self._aoi_model = self.martin_ruiz_aoi_loss
919+
elif model == 'interp':
920+
self._aoi_model = self.interp_aoi_loss
920921
elif model == 'no_loss':
921922
self._aoi_model = self.no_aoi_loss
922923
else:
@@ -928,22 +929,24 @@ def infer_aoi_model(self):
928929
module_parameters = tuple(
929930
array.module_parameters for array in self.system.arrays)
930931
params = _common_keys(module_parameters)
931-
if {'K', 'L', 'n'} <= params:
932+
if iam._IAM_MODEL_PARAMS['physical'] <= params:
932933
return self.physical_aoi_loss
933-
elif {'B5', 'B4', 'B3', 'B2', 'B1', 'B0'} <= params:
934+
elif iam._IAM_MODEL_PARAMS['sapm'] <= params:
934935
return self.sapm_aoi_loss
935-
elif {'b'} <= params:
936+
elif iam._IAM_MODEL_PARAMS['ashrae'] <= params:
936937
return self.ashrae_aoi_loss
937-
elif {'a_r'} <= params:
938+
elif iam._IAM_MODEL_PARAMS['martin_ruiz'] <= params:
938939
return self.martin_ruiz_aoi_loss
940+
elif iam._IAM_MODEL_PARAMS['interp'] <= params:
941+
return self.interp_aoi_loss
939942
else:
940943
raise ValueError('could not infer AOI model from '
941944
'system.arrays[i].module_parameters. Check that '
942945
'the module_parameters for all Arrays in '
943-
'system.arrays contain parameters for '
944-
'the physical, aoi, ashrae or martin_ruiz model; '
945-
'explicitly set the model with the aoi_model '
946-
'kwarg; or set aoi_model="no_loss".')
946+
'system.arrays contain parameters for the '
947+
'physical, aoi, ashrae, martin_ruiz or interp '
948+
'model; explicitly set the model with the '
949+
'aoi_model kwarg; or set aoi_model="no_loss".')
947950

948951
def ashrae_aoi_loss(self):
949952
self.results.aoi_modifier = self.system.get_iam(
@@ -972,6 +975,13 @@ def martin_ruiz_aoi_loss(self):
972975
)
973976
return self
974977

978+
def interp_aoi_loss(self):
979+
self.results.aoi_modifier = self.system.get_iam(
980+
self.results.aoi,
981+
iam_model='interp'
982+
)
983+
return self
984+
975985
def no_aoi_loss(self):
976986
if self.system.num_arrays == 1:
977987
self.results.aoi_modifier = 1.0

pvlib/pvsystem.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import io
99
import itertools
1010
import os
11+
import inspect
1112
from urllib.request import urlopen
1213
import numpy as np
1314
from scipy import constants
@@ -388,7 +389,7 @@ def get_iam(self, aoi, iam_model='physical'):
388389
389390
aoi_model : string, default 'physical'
390391
The IAM model to be used. Valid strings are 'physical', 'ashrae',
391-
'martin_ruiz' and 'sapm'.
392+
'martin_ruiz', 'sapm' and 'interp'.
392393
Returns
393394
-------
394395
iam : numeric or tuple of numeric
@@ -1151,7 +1152,7 @@ def get_iam(self, aoi, iam_model='physical'):
11511152
11521153
aoi_model : string, default 'physical'
11531154
The IAM model to be used. Valid strings are 'physical', 'ashrae',
1154-
'martin_ruiz' and 'sapm'.
1155+
'martin_ruiz', 'sapm' and 'interp'.
11551156
11561157
Returns
11571158
-------
@@ -1164,16 +1165,16 @@ def get_iam(self, aoi, iam_model='physical'):
11641165
if `iam_model` is not a valid model name.
11651166
"""
11661167
model = iam_model.lower()
1167-
if model in ['ashrae', 'physical', 'martin_ruiz']:
1168-
param_names = iam._IAM_MODEL_PARAMS[model]
1169-
kwargs = _build_kwargs(param_names, self.module_parameters)
1170-
func = getattr(iam, model)
1168+
if model in ['ashrae', 'physical', 'martin_ruiz', 'interp']:
1169+
func = getattr(iam, model) # get function at pvlib.iam
1170+
# get all parameters from function signature to retrieve them from
1171+
# module_parameters if present
1172+
params = set(inspect.signature(func).parameters.keys())
1173+
params.discard('aoi') # exclude aoi so it can't be repeated
1174+
kwargs = _build_kwargs(params, self.module_parameters)
11711175
return func(aoi, **kwargs)
11721176
elif model == 'sapm':
11731177
return iam.sapm(aoi, self.module_parameters)
1174-
elif model == 'interp':
1175-
raise ValueError(model + ' is not implemented as an IAM model '
1176-
'option for Array')
11771178
else:
11781179
raise ValueError(model + ' is not a valid IAM model')
11791180

pvlib/tests/test_modelchain.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1455,6 +1455,27 @@ def test_aoi_model_no_loss(sapm_dc_snl_ac_system, location, weather):
14551455
assert mc.results.ac[1] < 1
14561456

14571457

1458+
def test_aoi_model_interp(sapm_dc_snl_ac_system, location, weather, mocker):
1459+
# similar to test_aoi_models but requires arguments to work, so we
1460+
# add 'interp' aoi losses model arguments to module
1461+
iam_ref = (1., 0.85)
1462+
theta_ref = (0., 80.)
1463+
sapm_dc_snl_ac_system.arrays[0].module_parameters['iam_ref'] = iam_ref
1464+
sapm_dc_snl_ac_system.arrays[0].module_parameters['theta_ref'] = theta_ref
1465+
mc = ModelChain(sapm_dc_snl_ac_system, location,
1466+
dc_model='sapm', aoi_model='interp',
1467+
spectral_model='no_loss')
1468+
m = mocker.spy(iam, 'interp')
1469+
mc.run_model(weather=weather)
1470+
# only test kwargs
1471+
assert m.call_args[1]['iam_ref'] == iam_ref
1472+
assert m.call_args[1]['theta_ref'] == theta_ref
1473+
assert isinstance(mc.results.ac, pd.Series)
1474+
assert not mc.results.ac.empty
1475+
assert mc.results.ac[0] > 150 and mc.results.ac[0] < 200
1476+
assert mc.results.ac[1] < 1
1477+
1478+
14581479
def test_aoi_model_user_func(sapm_dc_snl_ac_system, location, weather, mocker):
14591480
m = mocker.spy(sys.modules[__name__], 'constant_aoi_loss')
14601481
mc = ModelChain(sapm_dc_snl_ac_system, location, dc_model='sapm',
@@ -1468,7 +1489,7 @@ def test_aoi_model_user_func(sapm_dc_snl_ac_system, location, weather, mocker):
14681489

14691490

14701491
@pytest.mark.parametrize('aoi_model', [
1471-
'sapm', 'ashrae', 'physical', 'martin_ruiz'
1492+
'sapm', 'ashrae', 'physical', 'martin_ruiz', 'interp'
14721493
])
14731494
def test_infer_aoi_model(location, system_no_aoi, aoi_model):
14741495
for k in iam._IAM_MODEL_PARAMS[aoi_model]:
@@ -1477,6 +1498,26 @@ def test_infer_aoi_model(location, system_no_aoi, aoi_model):
14771498
assert isinstance(mc, ModelChain)
14781499

14791500

1501+
@pytest.mark.parametrize('aoi_model,model_kwargs', [
1502+
# model_kwargs has both required and optional kwargs; test all
1503+
('physical',
1504+
{'n': 1.526, 'K': 4.0, 'L': 0.002, # required
1505+
'n_ar': 1.8}), # extra
1506+
('interp',
1507+
{'theta_ref': (0, 75, 85, 90), 'iam_ref': (1, 0.8, 0.42, 0), # required
1508+
'method': 'cubic', 'normalize': False})]) # extra
1509+
def test_infer_aoi_model_with_extra_params(location, system_no_aoi, aoi_model,
1510+
model_kwargs, weather, mocker):
1511+
# test extra parameters not defined at iam._IAM_MODEL_PARAMS are passed
1512+
m = mocker.spy(iam, aoi_model)
1513+
system_no_aoi.arrays[0].module_parameters.update(**model_kwargs)
1514+
mc = ModelChain(system_no_aoi, location, spectral_model='no_loss')
1515+
assert isinstance(mc, ModelChain)
1516+
mc.run_model(weather=weather)
1517+
_, call_kwargs = m.call_args
1518+
assert call_kwargs == model_kwargs
1519+
1520+
14801521
def test_infer_aoi_model_invalid(location, system_no_aoi):
14811522
exc_text = 'could not infer AOI model'
14821523
with pytest.raises(ValueError, match=exc_text):

pvlib/tests/test_pvsystem.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,15 @@ def test_PVSystem_get_iam_sapm(sapm_module_params, mocker):
6464
assert_allclose(out, 1.0, atol=0.01)
6565

6666

67-
def test_PVSystem_get_iam_interp(sapm_module_params, mocker):
68-
system = pvsystem.PVSystem(module_parameters=sapm_module_params)
69-
with pytest.raises(ValueError):
70-
system.get_iam(45, iam_model='interp')
67+
def test_PVSystem_get_iam_interp(mocker):
68+
interp_module_params = {'iam_ref': (1., 0.8), 'theta_ref': (0., 80.)}
69+
system = pvsystem.PVSystem(module_parameters=interp_module_params)
70+
spy = mocker.spy(_iam, 'interp')
71+
aoi = ((0., 40., 80.),)
72+
expected = (1., 0.9, 0.8)
73+
out = system.get_iam(aoi, iam_model='interp')
74+
assert_allclose(out, expected)
75+
spy.assert_called_once_with(aoi[0], **interp_module_params)
7176

7277

7378
def test__normalize_sam_product_names():

0 commit comments

Comments
 (0)