Skip to content

Commit c64e6b9

Browse files
Harmonize IAM and spectral correction and update IAM inference
1 parent 0b4a91d commit c64e6b9

File tree

7 files changed

+263
-156
lines changed

7 files changed

+263
-156
lines changed

pvlib/iam.py

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

1919

20-
def _get_builtin_models():
21-
"""
22-
Get builtin IAM models' usage information.
23-
24-
Returns
25-
-------
26-
info : dict
27-
A dictionary of dictionaries keyed by builtin IAM model name, with
28-
each model dictionary containing:
29-
30-
* 'func': callable
31-
The callable model function
32-
* 'params_required': set of str
33-
The model function's required parameters
34-
* 'params_optional': set of str
35-
The model function's optional parameters
36-
37-
See Also
38-
--------
39-
pvlib.iam.ashrae
40-
pvlib.iam.interp
41-
pvlib.iam.martin_ruiz
42-
pvlib.iam.physical
43-
pvlib.iam.sapm
44-
pvlib.iam.schlick
45-
"""
46-
return {
47-
'ashrae': {
48-
'func': ashrae,
49-
'params_required': set(),
50-
'params_optional': {'b'},
51-
},
52-
'interp': {
53-
'func': interp,
54-
'params_required': {'theta_ref', 'iam_ref'},
55-
'params_optional': {'method', 'normalize'},
56-
},
57-
'martin_ruiz': {
58-
'func': martin_ruiz,
59-
'params_required': set(),
60-
'params_optional': {'a_r'},
61-
},
62-
'physical': {
63-
'func': physical,
64-
'params_required': set(),
65-
'params_optional': {'n', 'K', 'L', 'n_ar'},
66-
},
67-
'sapm': {
68-
'func': sapm,
69-
'params_required': {'B0', 'B1', 'B2', 'B3', 'B4', 'B5'},
70-
'params_optional': {'upper'},
71-
},
72-
'schlick': {
73-
'func': schlick,
74-
'params_required': set(),
75-
'params_optional': set(),
76-
},
77-
}
78-
79-
8020
def ashrae(aoi, b=0.05):
8121
r"""
8222
Determine the incidence angle modifier using the ASHRAE transmission
@@ -370,7 +310,7 @@ def martin_ruiz(aoi, a_r=0.16):
370310

371311
def martin_ruiz_diffuse(surface_tilt, a_r=0.16, c1=0.4244, c2=None):
372312
'''
373-
Determine the incidence angle modifiers (iam) for diffuse sky and
313+
Determine the incidence angle modifiers (IAM) for diffuse sky and
374314
ground-reflected irradiance using the Martin and Ruiz incident angle model.
375315
376316
Parameters
@@ -557,54 +497,88 @@ def interp(aoi, theta_ref, iam_ref, method='linear', normalize=True):
557497

558498
def sapm(aoi, B0, B1, B2, B3, B4, B5, upper=None):
559499
r"""
560-
Determine the incidence angle modifier (IAM) using the SAPM model.
500+
Caclulate the incidence angle modifier (IAM), :math:`f_2`, using the
501+
Sandia Array Performance Model (SAPM).
502+
503+
The SAPM incidence angle modifier is part of the broader Sandia Array
504+
Performance Model, which defines five points on an IV curve using empirical
505+
module-specific coefficients. Module coefficients for the SAPM are
506+
available in the SAPM database and can be retrieved for use through
507+
:py:func:`pvlib.pvsystem.retrieve_sam()`. More details on the SAPM can be
508+
found in [1]_, while a full description of the procedure to determine the
509+
empirical model coefficients, including those for the SAPM incidence angle
510+
modifier, can be found in [2]_.
561511
562512
Parameters
563513
----------
564514
aoi : numeric
565515
Angle of incidence in degrees. Negative input angles will return
566516
zeros.
567517
568-
B0 : The coefficient of the degree-0 polynomial term.
518+
B0 : float
519+
The coefficient of the degree-0 polynomial term.
569520
570-
B1 : The coefficient of the degree-1 polynomial term.
521+
B1 : float
522+
The coefficient of the degree-1 polynomial term.
571523
572-
B2 : The coefficient of the degree-2 polynomial term.
524+
B2 : float
525+
The coefficient of the degree-2 polynomial term.
573526
574-
B3 : The coefficient of the degree-3 polynomial term.
527+
B3 : float
528+
The coefficient of the degree-3 polynomial term.
575529
576-
B4 : The coefficient of the degree-4 polynomial term.
530+
B4 : float
531+
The coefficient of the degree-4 polynomial term.
577532
578-
B5 : The coefficient of the degree-5 polynomial term.
533+
B5 : float
534+
The coefficient of the degree-5 polynomial term.
579535
580536
upper : float, optional
581537
Upper limit on the results. None means no upper limiting.
582538
583539
Returns
584540
-------
585541
iam : numeric
586-
The SAPM angle of incidence loss coefficient, termed F2 in [1]_.
542+
The SAPM angle of incidence loss coefficient, :math:`f_2` in [1]_.
587543
588544
Notes
589545
-----
546+
The SAPM spectral correction functions parameterises :math:`f_2` as a
547+
fifth-order polynomial function of angle of incidence:
548+
549+
.. math::
550+
551+
f_2 = b_0 + b_1 AOI + b_2 AOI^2 + b_3 AOI^3 + b_4 AOI^4 + b_5 AOI^5.
552+
553+
where :math:`f_2` is the spectral mismatch factor, :math:`b_{0-5}` are
554+
the module-specific coefficients, and :math:`AOI` is the angle of
555+
incidence. More detail on how this incidence angle modifier function was
556+
developed can be found in [3]_. Its measurement is described in [4]_.
557+
590558
The SAPM [1]_ traditionally does not define an upper limit on the AOI
591559
loss function and values slightly exceeding 1 may exist for moderate
592560
angles of incidence (15-40 degrees). However, users may consider
593561
imposing an upper limit of 1.
594562
595563
References
596564
----------
597-
.. [1] King, D. et al, 2004, "Sandia Photovoltaic Array Performance
598-
Model", SAND Report 3535, Sandia National Laboratories, Albuquerque,
599-
NM.
600-
601-
.. [2] B.H. King et al, "Procedure to Determine Coefficients for the
602-
Sandia Array Performance Model (SAPM)," SAND2016-5284, Sandia
603-
National Laboratories (2016).
604-
605-
.. [3] B.H. King et al, "Recent Advancements in Outdoor Measurement
606-
Techniques for Angle of Incidence Effects," 42nd IEEE PVSC (2015).
607-
:doi:`10.1109/PVSC.2015.7355849`
565+
.. [1] King, D., Kratochvil, J., and Boyson W. (2004), "Sandia
566+
Photovoltaic Array Performance Model", (No. SAND2004-3535), Sandia
567+
National Laboratories, Albuquerque, NM (United States).
568+
:doi:`10.2172/919131`
569+
.. [2] King, B., Hansen, C., Riley, D., Robinson, C., and Pratt, L.
570+
(2016). Procedure to determine coefficients for the Sandia Array
571+
Performance Model (SAPM) (No. SAND2016-5284). Sandia National
572+
Laboratories, Albuquerque, NM (United States).
573+
:doi:`10.2172/1256510`
574+
.. [3] King, D., Kratochvil, J., and Boyson, W. "Measuring solar spectral
575+
and angle-of-incidence effects on photovoltaic modules and solar
576+
irradiance sensors." Conference Record of the 26th IEEE Potovoltaic
577+
Specialists Conference (PVSC). IEEE, 1997.
578+
:doi:`10.1109/PVSC.1997.654283`
579+
.. [4] B.H. King et al, "Recent Advancements in Outdoor Measurement
580+
Techniques for Angle of Incidence Effects," 42nd IEEE PVSC (2015).
581+
:doi:`10.1109/PVSC.2015.7355849`
608582
609583
See Also
610584
--------
@@ -1022,6 +996,66 @@ def schlick_diffuse(surface_tilt):
1022996
return cuk, cug
1023997

1024998

999+
def _get_builtin_models():
1000+
"""
1001+
Get builtin IAM models' usage information.
1002+
1003+
Returns
1004+
-------
1005+
info : dict
1006+
A dictionary of dictionaries keyed by builtin IAM model name, with
1007+
each model dictionary containing:
1008+
1009+
* 'func': callable
1010+
The callable model function
1011+
* 'params_required': set of str
1012+
The model function's required parameters
1013+
* 'params_optional': set of str
1014+
The model function's optional parameters
1015+
1016+
See Also
1017+
--------
1018+
pvlib.iam.ashrae
1019+
pvlib.iam.interp
1020+
pvlib.iam.martin_ruiz
1021+
pvlib.iam.physical
1022+
pvlib.iam.sapm
1023+
pvlib.iam.schlick
1024+
"""
1025+
return {
1026+
'ashrae': {
1027+
'func': ashrae,
1028+
'params_required': set(),
1029+
'params_optional': {'b'},
1030+
},
1031+
'interp': {
1032+
'func': interp,
1033+
'params_required': {'theta_ref', 'iam_ref'},
1034+
'params_optional': {'method', 'normalize'},
1035+
},
1036+
'martin_ruiz': {
1037+
'func': martin_ruiz,
1038+
'params_required': set(),
1039+
'params_optional': {'a_r'},
1040+
},
1041+
'physical': {
1042+
'func': physical,
1043+
'params_required': set(),
1044+
'params_optional': {'n', 'K', 'L', 'n_ar'},
1045+
},
1046+
'sapm': {
1047+
'func': sapm,
1048+
'params_required': {'B0', 'B1', 'B2', 'B3', 'B4', 'B5'},
1049+
'params_optional': {'upper'},
1050+
},
1051+
'schlick': {
1052+
'func': schlick,
1053+
'params_required': set(),
1054+
'params_optional': set(),
1055+
},
1056+
}
1057+
1058+
10251059
def _get_fittable_or_convertable_model(builtin_model_name):
10261060
# check that model is implemented and fittable or convertable
10271061
implemented_builtin_models = {

pvlib/modelchain.py

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -782,62 +782,59 @@ def aoi_model(self, model):
782782
self._aoi_model = self.schlick_aoi_loss
783783
else:
784784
raise ValueError(model + ' is not a valid aoi loss model')
785-
else:
786-
# Assume callable model.
785+
elif callable(model):
787786
self._aoi_model = partial(model, self)
787+
else:
788+
raise ValueError(model + ' is not a valid aoi loss model')
788789

789790
def infer_aoi_model(self):
790791
"""
791-
Infer AOI model by checking for at least one required or optional
792-
model parameter in module_parameter collected across all arrays.
792+
Infer AOI model by checking for all required model paramters or at
793+
least one optional model parameter in module_parameter collected
794+
across all arrays.
793795
"""
794796
module_parameters = tuple(
795797
array.module_parameters for array in self.system.arrays
796798
)
797799
params = _common_keys(module_parameters)
798800
builtin_models = pvlib.iam._get_builtin_models()
799801

800-
if any(
801-
param in params for
802-
param in builtin_models['ashrae']["params_required"].union(
803-
builtin_models['ashrae']["params_optional"]
804-
)
802+
if (builtin_models['ashrae']["params_required"] and (
803+
builtin_models['ashrae']["params_required"] <= params)) or (
804+
not builtin_models['ashrae']["params_required"] and
805+
(builtin_models['ashrae']["params_optional"] & params)
805806
):
806807
return self.ashrae_aoi_loss
807808

808-
if any(
809-
param in params for
810-
param in builtin_models['interp']["params_required"].union(
811-
builtin_models['interp']["params_optional"]
812-
)
809+
if (builtin_models['interp']["params_required"] and (
810+
builtin_models['interp']["params_required"] <= params)) or (
811+
not builtin_models['interp']["params_required"] and
812+
(builtin_models['interp']["params_optional"] & params)
813813
):
814814
return self.interp_aoi_loss
815815

816-
if any(
817-
param in params for
818-
param in builtin_models['martin_ruiz']["params_required"].union(
819-
builtin_models['martin_ruiz']["params_optional"]
820-
)
816+
if (builtin_models['martin_ruiz']["params_required"] and (
817+
builtin_models['martin_ruiz']["params_required"] <= params)) or (
818+
not builtin_models['martin_ruiz']["params_required"] and
819+
(builtin_models['martin_ruiz']["params_optional"] & params)
821820
):
822821
return self.martin_ruiz_aoi_loss
823822

824-
if any(
825-
param in params for
826-
param in builtin_models['physical']["params_required"].union(
827-
builtin_models['physical']["params_optional"]
828-
)
823+
if (builtin_models['physical']["params_required"] and (
824+
builtin_models['physical']["params_required"] <= params)) or (
825+
not builtin_models['physical']["params_required"] and
826+
(builtin_models['physical']["params_optional"] & params)
829827
):
830828
return self.physical_aoi_loss
831829

832-
if any(
833-
param in params for
834-
param in builtin_models['sapm']["params_required"].union(
835-
builtin_models['sapm']["params_optional"]
836-
)
830+
if (builtin_models['sapm']["params_required"] and (
831+
builtin_models['sapm']["params_required"] <= params)) or (
832+
not builtin_models['sapm']["params_required"] and
833+
(builtin_models['sapm']["params_optional"] & params)
837834
):
838835
return self.sapm_aoi_loss
839836

840-
# schlick model has no parameters to distinguish.
837+
# schlick model has no parameters to distinguish, esp. from no_loss.
841838

842839
raise ValueError(
843840
'could not infer AOI model from '
@@ -909,8 +906,10 @@ def spectral_model(self, model):
909906
self._spectral_model = self.no_spectral_loss
910907
else:
911908
raise ValueError(model + ' is not a valid spectral loss model')
912-
else:
909+
elif callable(model):
913910
self._spectral_model = partial(model, self)
911+
else:
912+
raise ValueError(model + ' is not a valid spectral loss model')
914913

915914
def infer_spectral_model(self):
916915
"""Infer spectral model from system attributes."""

pvlib/pvsystem.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -633,9 +633,14 @@ def sapm_spectral_loss(self, airmass_absolute):
633633
The SAPM spectral loss coefficient.
634634
"""
635635
return tuple(
636-
spectrum.spectral_factor_sapm(airmass_absolute,
637-
array.module_parameters)
638-
for array in self.arrays
636+
spectrum.spectral_factor_sapm(
637+
airmass_absolute,
638+
array.module_parameters["A0"],
639+
array.module_parameters["A1"],
640+
array.module_parameters["A2"],
641+
array.module_parameters["A3"],
642+
array.module_parameters["A4"],
643+
) for array in self.arrays
639644
)
640645

641646
@_unwrap_single_value
@@ -2390,7 +2395,14 @@ def sapm_effective_irradiance(poa_direct, poa_diffuse, airmass_absolute, aoi,
23902395
pvlib.pvsystem.sapm
23912396
"""
23922397

2393-
F1 = spectrum.spectral_factor_sapm(airmass_absolute, module)
2398+
F1 = spectrum.spectral_factor_sapm(
2399+
airmass_absolute,
2400+
module["A0"],
2401+
module["A1"],
2402+
module["A2"],
2403+
module["A3"],
2404+
module["A4"],
2405+
)
23942406
F2 = iam.sapm(
23952407
aoi,
23962408
module["B0"],

0 commit comments

Comments
 (0)