Skip to content

Commit ec3790b

Browse files
Make sapm iam function use common interface
1 parent a037bae commit ec3790b

File tree

7 files changed

+100
-68
lines changed

7 files changed

+100
-68
lines changed

pvlib/iam.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from pvlib.tools import cosd, sind, acosd
1818

1919

20-
def get_builtin_models():
20+
def _get_builtin_models():
2121
"""
2222
Get builtin IAM models' usage information.
2323
@@ -66,8 +66,6 @@ def get_builtin_models():
6666
},
6767
'sapm': {
6868
'func': sapm,
69-
# Exceptional interface: Parameters inside params_required must
70-
# appear in the required module dictionary parameter.
7169
'params_required': {'B0', 'B1', 'B2', 'B3', 'B4', 'B5'},
7270
'params_optional': {'upper'},
7371
},
@@ -557,7 +555,7 @@ def interp(aoi, theta_ref, iam_ref, method='linear', normalize=True):
557555
return iam
558556

559557

560-
def sapm(aoi, module, upper=None):
558+
def sapm(aoi, B0, B1, B2, B3, B4, B5, upper=None):
561559
r"""
562560
Determine the incidence angle modifier (IAM) using the SAPM model.
563561
@@ -567,9 +565,17 @@ def sapm(aoi, module, upper=None):
567565
Angle of incidence in degrees. Negative input angles will return
568566
zeros.
569567
570-
module : dict-like
571-
A dict or Series with the SAPM IAM model parameters.
572-
See the :py:func:`sapm` notes section for more details.
568+
B0 : The coefficient of the degree-0 polynomial term.
569+
570+
B1 : The coefficient of the degree-1 polynomial term.
571+
572+
B2 : The coefficient of the degree-2 polynomial term.
573+
574+
B3 : The coefficient of the degree-3 polynomial term.
575+
576+
B4 : The coefficient of the degree-4 polynomial term.
577+
578+
B5 : The coefficient of the degree-5 polynomial term.
573579
574580
upper : float, optional
575581
Upper limit on the results. None means no upper limiting.
@@ -609,10 +615,7 @@ def sapm(aoi, module, upper=None):
609615
pvlib.iam.schlick
610616
"""
611617

612-
aoi_coeff = [module['B5'], module['B4'], module['B3'], module['B2'],
613-
module['B1'], module['B0']]
614-
615-
iam = np.polyval(aoi_coeff, aoi)
618+
iam = np.polyval([B5, B4, B3, B2, B1, B0], aoi)
616619
iam = np.clip(iam, 0, upper)
617620
# nan tolerant masking
618621
aoi_lt_0 = np.full_like(aoi, False, dtype='bool')
@@ -665,6 +668,7 @@ def marion_diffuse(model, surface_tilt, **kwargs):
665668
pvlib.iam.interp
666669
pvlib.iam.martin_ruiz
667670
pvlib.iam.physical
671+
pvlib.iam.sapm
668672
pvlib.iam.schlick
669673
pvlib.iam.martin_ruiz_diffuse
670674
pvlib.iam.schlick_diffuse
@@ -693,7 +697,7 @@ def marion_diffuse(model, surface_tilt, **kwargs):
693697
func = model
694698
else:
695699
# Check that a builtin IAM function was specified.
696-
builtin_models = get_builtin_models()
700+
builtin_models = _get_builtin_models()
697701

698702
try:
699703
model = builtin_models[model]
@@ -1035,7 +1039,7 @@ def _get_fittable_or_convertable_model(builtin_model_name):
10351039
def _check_params(builtin_model_name, params):
10361040
# check that parameters passed in with IAM model belong to the model
10371041
handed_params = set(params.keys())
1038-
builtin_model = get_builtin_models()[builtin_model_name]
1042+
builtin_model = _get_builtin_models()[builtin_model_name]
10391043
expected_params = builtin_model["params_required"].union(
10401044
builtin_model["params_optional"]
10411045
)

pvlib/modelchain.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
the time to read the source code for the module.
77
"""
88

9+
from dataclasses import dataclass, field
910
from functools import partial
1011
import itertools
12+
from typing import Optional, Tuple, TypeVar, Union
1113
import warnings
14+
1215
import pandas as pd
13-
from dataclasses import dataclass, field
14-
from typing import Union, Tuple, Optional, TypeVar
1516

1617
from pvlib import pvsystem
1718
import pvlib.irradiance # avoid name conflict with full import
@@ -794,7 +795,7 @@ def infer_aoi_model(self):
794795
array.module_parameters for array in self.system.arrays
795796
)
796797
params = _common_keys(module_parameters)
797-
builtin_models = pvlib.iam.get_builtin_models()
798+
builtin_models = pvlib.iam._get_builtin_models()
798799

799800
if any(
800801
param in params for

pvlib/pvsystem.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,28 +1192,22 @@ def get_iam(self, aoi, iam_model='physical'):
11921192
model = iam_model.lower()
11931193

11941194
try:
1195-
model_info = iam.get_builtin_models()[model]
1195+
model_info = iam._get_builtin_models()[model]
11961196
except KeyError as exc:
11971197
raise ValueError(f'{iam_model} is not a valid iam_model') from exc
11981198

11991199
for param in model_info["params_required"]:
12001200
if param not in self.module_parameters:
12011201
raise KeyError(f"{param} is missing in module_parameters")
12021202

1203-
params_optional = _build_kwargs(
1204-
model_info["params_optional"], self.module_parameters
1205-
)
1206-
1207-
if model == "sapm":
1208-
# sapm has exceptional interface requiring module_parameters.
1209-
return model_info["func"](
1210-
aoi, self.module_parameters, **params_optional
1211-
)
1212-
12131203
params_required = _build_kwargs(
12141204
model_info["params_required"], self.module_parameters
12151205
)
12161206

1207+
params_optional = _build_kwargs(
1208+
model_info["params_optional"], self.module_parameters
1209+
)
1210+
12171211
return model_info["func"](
12181212
aoi, **params_required, **params_optional
12191213
)
@@ -2397,7 +2391,15 @@ def sapm_effective_irradiance(poa_direct, poa_diffuse, airmass_absolute, aoi,
23972391
"""
23982392

23992393
F1 = spectrum.spectral_factor_sapm(airmass_absolute, module)
2400-
F2 = iam.sapm(aoi, module)
2394+
F2 = iam.sapm(
2395+
aoi,
2396+
module["B0"],
2397+
module["B1"],
2398+
module["B2"],
2399+
module["B3"],
2400+
module["B4"],
2401+
module["B5"],
2402+
)
24012403

24022404
Ee = F1 * (poa_direct * F2 + module['FD'] * poa_diffuse)
24032405

pvlib/tests/test_iam.py

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,17 @@
66
import inspect
77

88
import numpy as np
9+
from numpy.testing import assert_allclose
910
import pandas as pd
10-
1111
import pytest
12-
from .conftest import assert_series_equal
13-
from numpy.testing import assert_allclose
1412
import scipy.interpolate
1513

1614
from pvlib import iam as _iam
15+
from pvlib.tests.conftest import assert_series_equal
1716

1817

1918
def test_get_builtin_models():
20-
builtin_models = _iam.get_builtin_models()
19+
builtin_models = _iam._get_builtin_models()
2120

2221
models = set(builtin_models.keys())
2322
models_expected = {
@@ -28,26 +27,14 @@ def test_get_builtin_models():
2827
for model in models:
2928
builtin_model = builtin_models[model]
3029

31-
if model == "sapm":
32-
# sapm has exceptional interface requiring module_parameters.
33-
params_required_expected = set(
34-
k for k, v in inspect.signature(
35-
builtin_model["func"]
36-
).parameters.items() if v.default is inspect.Parameter.empty
37-
)
38-
assert {"aoi", "module"} == params_required_expected, model
39-
40-
assert builtin_model["params_required"] == \
41-
{'B0', 'B1', 'B2', 'B3', 'B4', 'B5'}, model
42-
else:
43-
params_required_expected = set(
44-
k for k, v in inspect.signature(
45-
builtin_model["func"]
46-
).parameters.items() if v.default is inspect.Parameter.empty
47-
)
48-
assert builtin_model["params_required"].union(
49-
{"aoi"}
50-
) == params_required_expected, model
30+
params_required_expected = set(
31+
k for k, v in inspect.signature(
32+
builtin_model["func"]
33+
).parameters.items() if v.default is inspect.Parameter.empty
34+
)
35+
assert builtin_model["params_required"].union(
36+
{"aoi"}
37+
) == params_required_expected, model
5138

5239
params_optional_expected = set(
5340
k for k, v in inspect.signature(
@@ -266,7 +253,15 @@ def test_iam_interp():
266253
])
267254
def test_sapm(sapm_module_params, aoi, expected):
268255

269-
out = _iam.sapm(aoi, sapm_module_params)
256+
out = _iam.sapm(
257+
aoi,
258+
sapm_module_params["B0"],
259+
sapm_module_params["B1"],
260+
sapm_module_params["B2"],
261+
sapm_module_params["B3"],
262+
sapm_module_params["B4"],
263+
sapm_module_params["B5"],
264+
)
270265

271266
if isinstance(aoi, pd.Series):
272267
assert_series_equal(out, expected, check_less_precise=4)
@@ -276,13 +271,38 @@ def test_sapm(sapm_module_params, aoi, expected):
276271

277272
def test_sapm_limits():
278273
module_parameters = {'B0': 5, 'B1': 0, 'B2': 0, 'B3': 0, 'B4': 0, 'B5': 0}
279-
assert _iam.sapm(1, module_parameters) == 5
274+
assert _iam.sapm(
275+
1,
276+
module_parameters["B0"],
277+
module_parameters["B1"],
278+
module_parameters["B2"],
279+
module_parameters["B3"],
280+
module_parameters["B4"],
281+
module_parameters["B5"],
282+
) == 5
280283

281284
module_parameters = {'B0': 5, 'B1': 0, 'B2': 0, 'B3': 0, 'B4': 0, 'B5': 0}
282-
assert _iam.sapm(1, module_parameters, upper=1) == 1
285+
assert _iam.sapm(
286+
1,
287+
module_parameters["B0"],
288+
module_parameters["B1"],
289+
module_parameters["B2"],
290+
module_parameters["B3"],
291+
module_parameters["B4"],
292+
module_parameters["B5"],
293+
upper=1,
294+
) == 1
283295

284296
module_parameters = {'B0': -5, 'B1': 0, 'B2': 0, 'B3': 0, 'B4': 0, 'B5': 0}
285-
assert _iam.sapm(1, module_parameters) == 0
297+
assert _iam.sapm(
298+
1,
299+
module_parameters["B0"],
300+
module_parameters["B1"],
301+
module_parameters["B2"],
302+
module_parameters["B3"],
303+
module_parameters["B4"],
304+
module_parameters["B5"],
305+
) == 0
286306

287307

288308
def test_marion_diffuse_model(mocker):

pvlib/tests/test_modelchain.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1500,10 +1500,11 @@ def test_aoi_model_user_func(sapm_dc_snl_ac_system, location, weather, mocker):
15001500

15011501

15021502
@pytest.mark.parametrize('aoi_model', [
1503+
# "schlick" omitted, cannot be distinguished from "no_loss" AOI model.
15031504
'ashrae', 'interp', 'martin_ruiz', 'physical', 'sapm'
15041505
])
15051506
def test_infer_aoi_model(location, system_no_aoi, aoi_model):
1506-
builtin_models = iam.get_builtin_models()[aoi_model]
1507+
builtin_models = iam._get_builtin_models()[aoi_model]
15071508
params = builtin_models["params_required"].union(
15081509
builtin_models["params_optional"]
15091510
)

pvlib/tests/test_pvsystem.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,7 @@ def test_PVSystem_get_iam(mocker, iam_model, model_params):
4343
system = pvsystem.PVSystem(module_parameters=model_params)
4444
thetas = 45
4545
iam = system.get_iam(thetas, iam_model=iam_model)
46-
if iam_model == "sapm":
47-
# sapm has exceptional interface.
48-
m.assert_called_with(thetas, model_params)
49-
else:
50-
m.assert_called_with(thetas, **model_params)
46+
m.assert_called_with(thetas, **model_params)
5147
assert 0 < iam < 1
5248

5349

@@ -102,7 +98,15 @@ def test_PVSystem_get_iam_sapm(sapm_module_params, mocker):
10298
mocker.spy(_iam, 'sapm')
10399
aoi = 0
104100
out = system.get_iam(aoi, 'sapm')
105-
_iam.sapm.assert_called_once_with(aoi, sapm_module_params)
101+
_iam.sapm.assert_called_once_with(
102+
aoi,
103+
sapm_module_params["B0"],
104+
sapm_module_params["B1"],
105+
sapm_module_params["B2"],
106+
sapm_module_params["B3"],
107+
sapm_module_params["B4"],
108+
sapm_module_params["B5"],
109+
)
106110
assert_allclose(out, 1.0, atol=0.01)
107111

108112

pyproject.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ description = "A set of functions and classes for simulating the performance of
99
authors = [
1010
{ name = "pvlib python Developers", email = "[email protected]" },
1111
]
12-
requires-python = ">=3.9"
12+
requires-python = ">=3.9, <3.13"
1313
dependencies = [
1414
'numpy >= 1.19.3',
1515
'pandas >= 1.3.0',
@@ -49,9 +49,9 @@ dynamic = ["version"]
4949
optional = [
5050
'cython',
5151
'ephem',
52-
'nrel-pysam',
53-
'numba >= 0.17.0',
54-
'solarfactors',
52+
'nrel-pysam; python_version < "3.13"',
53+
'numba >= 0.17.0; python_version < "3.13"',
54+
'solarfactors; python_version < "3.12"',
5555
'statsmodels',
5656
]
5757
doc = [
@@ -65,7 +65,7 @@ doc = [
6565
'pillow',
6666
'sphinx-toggleprompt == 0.5.2',
6767
'sphinx-favicon',
68-
'solarfactors',
68+
'solarfactors; python_version < "3.12"',
6969
'sphinx-hoverxref',
7070
]
7171
test = [

0 commit comments

Comments
 (0)