Skip to content

Commit 48ee685

Browse files
cbcrespoAdamRJensencwhanse
authored
Add support for diffuse IAM in the Array and PVSystem classes (#2845)
* Add return_components to isotropic * Change OrderedDict to dict * Add tests for isotropic with return_components=True * Add what's new entry * Add support for 'return_components' * Add tests * Raise error if return_components=True used with king or klucher * Update pvlib/irradiance.py Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> * Adjust docstrings * Fix list indentation * Add more tests * Minor fix * Add whatsnew entry * Add get_iam_diffuse to the Array and PVSystem classes * Set all IAM outputs to dict * Add whatsnew entry * Minor change to docstrings * Docstring changes * Add schlick_diffuse * Add error test * Add Dataframe support * Add whatsnew entry * Fix diffuse IAM kwargs * Add tests to fix codecov * Fix Array.get_iam_diffuse kwargs * Implement suggestions by cwhanse * Apply batched suggestions from code review Co-authored-by: Cliff Hansen <cwhanse@sandia.gov> * Implement suggestions by kandersolar * Remove marion_diffuse as default * Fix backticks --------- Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> Co-authored-by: Cliff Hansen <cwhanse@sandia.gov>
1 parent 3da6024 commit 48ee685

3 files changed

Lines changed: 221 additions & 16 deletions

File tree

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ Enhancements
8989
(:issue:`2828`, :pull:`2832`)
9090
* Allow variables from multiple datasets to be requested at once in
9191
:py:func:`~pvlib.iotools.get_merra2`. (:pull:`2839`)
92+
* Add support for diffuse IAM in the :py:class:`pvlib.pvsystem.Array` and
93+
:py:class:`pvlib.pvsystem.PVSystem` classes (see `pvlib.pvsystem.Array.get_iam_diffuse`
94+
and `pvlib.pvsystem.PVSystem.get_iam_diffuse`). (:issue:`2812`, :pull:`2845`)
9295

9396

9497
Documentation

pvlib/pvsystem.py

Lines changed: 148 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ def get_aoi(self, solar_zenith, solar_azimuth):
306306
@_unwrap_single_value
307307
def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
308308
dni_extra=None, airmass=None, albedo=None,
309-
model='haydavies', **kwargs):
309+
model='haydavies', diffuse_components=False, **kwargs):
310310
"""
311311
Uses :py:func:`pvlib.irradiance.get_total_irradiance` to
312312
calculate the plane of array irradiance components on the tilted
@@ -333,6 +333,11 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
333333
Ground surface albedo. [unitless]
334334
model : String, default 'haydavies'
335335
Irradiance model.
336+
diffuse_components : bool, default False
337+
If ``True``, returns the diffuse irradiance components available
338+
from the selected model (e.g., `poa_isotropic`,
339+
`poa_circumsolar`, `poa_horizon`).
340+
If ``False``, only the total diffuse irradiance is returned.
336341
337342
kwargs
338343
Extra parameters passed to
@@ -372,7 +377,9 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
372377
array.get_irradiance(solar_zenith, solar_azimuth,
373378
dni, ghi, dhi,
374379
dni_extra=dni_extra, airmass=airmass,
375-
albedo=albedo, model=model, **kwargs)
380+
albedo=albedo, model=model,
381+
diffuse_components=diffuse_components,
382+
**kwargs)
376383
for array, dni, ghi, dhi, albedo in zip(
377384
self.arrays, dni, ghi, dhi, albedo
378385
)
@@ -381,8 +388,8 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
381388
@_unwrap_single_value
382389
def get_iam(self, aoi, iam_model='physical'):
383390
"""
384-
Determine the incidence angle modifier using the method specified by
385-
``iam_model``.
391+
Determine the incidence angle modifier for direct irradiance
392+
using the method specified by ``iam_model``.
386393
387394
Parameters for the selected IAM model are expected to be in
388395
``PVSystem.module_parameters``. Default parameters are available for
@@ -410,6 +417,48 @@ def get_iam(self, aoi, iam_model='physical'):
410417
return tuple(array.get_iam(aoi, iam_model)
411418
for array, aoi in zip(self.arrays, aoi))
412419

420+
@_unwrap_single_value
421+
def get_iam_diffuse(self, surface_tilt, iam_model,
422+
marion_model=None, **kwargs):
423+
"""
424+
Determine the incidence angle modifier for diffuse irradiance using the
425+
method specified by ``iam_model``.
426+
427+
Parameters for the selected IAM model are expected to be in
428+
``Array.module_parameters``. Default parameters are available for
429+
the 'marion_diffuse' and 'martin_ruiz_diffuse' models.
430+
431+
Parameters
432+
----------
433+
surface_tilt : numeric or tuple of numeric
434+
The tilt angle of the surface in degrees.
435+
iam_model : str
436+
The IAM model to be used. Valid strings are 'marion_diffuse',
437+
'martin_ruiz_diffuse', and 'schlick_diffuse'.
438+
marion_model : str, optional
439+
The IAM function to evaluate across a solid angle. Only used when
440+
``iam_model='marion_diffuse'``. Must be one of 'ashrae',
441+
'physical', 'martin_ruiz', 'sapm', and 'schlick'.
442+
443+
kwargs : dict, optional
444+
Additional keyword arguments passed to the IAM model function.
445+
446+
Returns
447+
-------
448+
iam_diffuse : dict or DataFrame
449+
The AOI modifiers for different diffuse irradiance components.
450+
Included components depend on the selected ``iam_model``.
451+
452+
Raises
453+
------
454+
ValueError
455+
if `iam_model` is not a valid model name.
456+
"""
457+
surface_tilt = self._validate_per_array(surface_tilt)
458+
return tuple(array.get_iam_diffuse(tilt, iam_model=iam_model,
459+
marion_model=marion_model, **kwargs)
460+
for array, tilt in zip(self.arrays, surface_tilt))
461+
413462
@_unwrap_single_value
414463
def get_cell_temperature(self, poa_global, temp_air, wind_speed, model,
415464
effective_irradiance=None, longwave_down=None):
@@ -1092,7 +1141,7 @@ def get_aoi(self, solar_zenith, solar_azimuth):
10921141

10931142
def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
10941143
dni_extra=None, airmass=None, albedo=None,
1095-
model='haydavies', **kwargs):
1144+
model='haydavies', diffuse_components=False, **kwargs):
10961145
"""
10971146
Get plane of array irradiance components.
10981147
@@ -1120,6 +1169,11 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
11201169
Ground surface albedo. [unitless]
11211170
model : String, default 'haydavies'
11221171
Irradiance model.
1172+
diffuse_components : bool, default False
1173+
If ``True``, returns the diffuse irradiance components available
1174+
from the selected model (e.g., ``poa_isotropic``,
1175+
``poa_circumsolar``, ``poa_horizon``).
1176+
If ``False``, only the total diffuse irradiance is returned.
11231177
11241178
kwargs
11251179
Extra parameters passed to
@@ -1160,20 +1214,23 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
11601214
airmass = atmosphere.get_relative_airmass(solar_zenith)
11611215

11621216
orientation = self.mount.get_orientation(solar_zenith, solar_azimuth)
1163-
return irradiance.get_total_irradiance(orientation['surface_tilt'],
1164-
orientation['surface_azimuth'],
1165-
solar_zenith, solar_azimuth,
1166-
dni, ghi, dhi,
1167-
dni_extra=dni_extra,
1168-
airmass=airmass,
1169-
albedo=albedo,
1170-
model=model,
1171-
**kwargs)
1217+
return irradiance.get_total_irradiance(
1218+
orientation['surface_tilt'],
1219+
orientation['surface_azimuth'],
1220+
solar_zenith, solar_azimuth,
1221+
dni, ghi, dhi,
1222+
dni_extra=dni_extra,
1223+
airmass=airmass,
1224+
albedo=albedo,
1225+
model=model,
1226+
diffuse_components=diffuse_components,
1227+
**kwargs
1228+
)
11721229

11731230
def get_iam(self, aoi, iam_model='physical'):
11741231
"""
1175-
Determine the incidence angle modifier using the method specified by
1176-
``iam_model``.
1232+
Determine the incidence angle modifier for direct irradiance
1233+
using the method specified by ``iam_model``.
11771234
11781235
Parameters for the selected IAM model are expected to be in
11791236
``Array.module_parameters``. Default parameters are available for
@@ -1212,6 +1269,81 @@ def get_iam(self, aoi, iam_model='physical'):
12121269
else:
12131270
raise ValueError(model + ' is not a valid IAM model')
12141271

1272+
def get_iam_diffuse(self, surface_tilt, iam_model,
1273+
marion_model=None, **kwargs):
1274+
"""
1275+
Determine the incidence angle modifier for various diffuse irradiance
1276+
components using the method specified by ``iam_model``.
1277+
1278+
Parameters for ``iam_model`` are used from ``Array.module_parameters``
1279+
if found. If parameters are not found in ``Array.module_parameters``,
1280+
default parameters for ``iam_model`` are used.
1281+
1282+
Parameters
1283+
----------
1284+
surface_tilt : numeric
1285+
The tilt angle of the surface in degrees.
1286+
iam_model : str
1287+
The IAM model to be used. Valid strings are 'marion_diffuse',
1288+
'martin_ruiz_diffuse' and 'schlick_diffuse'.
1289+
marion_model : str, optional
1290+
The IAM function to evaluate across a solid angle. Only used when
1291+
``iam_model='marion_diffuse'``. Must be one of 'ashrae',
1292+
'physical', 'martin_ruiz', 'sapm', and 'schlick'.
1293+
1294+
Returns
1295+
-------
1296+
iam_diffuse : dict or DataFrame
1297+
The AOI modifiers for different diffuse irradiance components.
1298+
Included components depend on the selected ``iam_model``.
1299+
1300+
Raises
1301+
------
1302+
ValueError
1303+
if ``iam_model`` is not a valid model name.
1304+
ValueError
1305+
if ``iam_model`` is 'marion_diffuse' and ``marion_model`` is not
1306+
a valid model name.
1307+
"""
1308+
model = iam_model.lower()
1309+
if model == 'marion_diffuse' and marion_model is None:
1310+
raise ValueError('marion_model must be specified when '
1311+
'iam_model="marion_diffuse"')
1312+
if model == 'marion_diffuse':
1313+
if marion_model in ['ashrae', 'physical', 'martin_ruiz',
1314+
'schlick']:
1315+
func = getattr(iam, marion_model)
1316+
params = iam._IAM_MODEL_PARAMS[marion_model]
1317+
params.discard('aoi')
1318+
kwargs = _build_kwargs(params, self.module_parameters)
1319+
iams = iam.marion_diffuse(model=marion_model,
1320+
surface_tilt=surface_tilt,
1321+
**kwargs)
1322+
elif marion_model == 'sapm':
1323+
iams = iam.marion_diffuse(model='sapm',
1324+
surface_tilt=surface_tilt,
1325+
module=self.module_parameters,
1326+
**kwargs)
1327+
else:
1328+
raise ValueError(marion_model + ' is not a valid IAM model')
1329+
elif model == 'martin_ruiz_diffuse':
1330+
func = getattr(iam, model) # get function at pvlib.iam
1331+
# get all parameters from function signature to retrieve them from
1332+
# module_parameters if present
1333+
params = set(inspect.signature(func).parameters.keys())
1334+
params.discard('aoi')
1335+
kwargs = _build_kwargs(params, self.module_parameters)
1336+
iams = iam.martin_ruiz_diffuse(surface_tilt=surface_tilt, **kwargs)
1337+
elif model == 'schlick_diffuse':
1338+
iams = iam.schlick_diffuse(surface_tilt=surface_tilt)
1339+
else:
1340+
raise ValueError(model + ' is not a valid diffuse IAM model')
1341+
1342+
if isinstance(surface_tilt, pd.Series):
1343+
iams = pd.DataFrame(iams, index=surface_tilt.index)
1344+
1345+
return iams
1346+
12151347
def get_cell_temperature(self, poa_global, temp_air, wind_speed, model,
12161348
effective_irradiance=None, longwave_down=None):
12171349
"""

tests/test_pvsystem.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,76 @@ def test_PVSystem_get_iam_invalid(sapm_module_params, mocker):
112112
system.get_iam(45, iam_model='not_a_model')
113113

114114

115+
def test_PVSystem_get_iam_diffuse_marion(sapm_module_params, mocker):
116+
model_params = {'b': 0.05}
117+
m = mocker.spy(_iam, 'marion_diffuse')
118+
system = pvsystem.PVSystem(module_parameters=model_params)
119+
tilt = 30
120+
iam = system.get_iam_diffuse(tilt, iam_model='marion_diffuse',
121+
marion_model='ashrae')
122+
m.assert_called_with(model='ashrae', surface_tilt=tilt,
123+
**model_params)
124+
assert isinstance(iam, dict)
125+
assert set(iam.keys()) == {'sky', 'ground', 'horizon'}
126+
127+
system = pvsystem.PVSystem(module_parameters=sapm_module_params)
128+
tilt = pd.Series([30, 60])
129+
iam = system.get_iam_diffuse(tilt, iam_model='marion_diffuse',
130+
marion_model='sapm')
131+
assert isinstance(iam, pd.DataFrame)
132+
133+
134+
@pytest.mark.parametrize('iam_model', ['martin_ruiz_diffuse',
135+
'schlick_diffuse'])
136+
def test_PVSystem_get_iam_diffuse(iam_model, mocker):
137+
model_params = {'a_r': 0.16} if iam_model == 'martin_ruiz_diffuse' else {}
138+
m = mocker.spy(_iam, iam_model)
139+
system = pvsystem.PVSystem(module_parameters=model_params)
140+
tilt = 30
141+
iam = system.get_iam_diffuse(tilt, iam_model=iam_model)
142+
m.assert_called_with(surface_tilt=tilt, **model_params)
143+
assert isinstance(iam, dict)
144+
145+
146+
def test_PVSystem_multi_array_get_iam_diffuse():
147+
model_params = {'b': 0.05}
148+
system = pvsystem.PVSystem(
149+
arrays=[pvsystem.Array(mount=pvsystem.FixedMount(0, 180),
150+
module_parameters=model_params),
151+
pvsystem.Array(mount=pvsystem.FixedMount(0, 180),
152+
module_parameters=model_params)]
153+
)
154+
iam = system.get_iam_diffuse((30, 60), iam_model='marion_diffuse',
155+
marion_model='ashrae')
156+
assert len(iam) == 2
157+
assert iam[0] != iam[1]
158+
with pytest.raises(ValueError,
159+
match="Length mismatch for per-array parameter"):
160+
system.get_iam_diffuse((30,), iam_model='marion_diffuse',
161+
marion_model='ashrae')
162+
163+
164+
def test_PVSystem_get_iam_diffuse_invalid(sapm_module_params):
165+
system = pvsystem.PVSystem(module_parameters=sapm_module_params)
166+
msg = 'not a valid diffuse IAM model'
167+
with pytest.raises(ValueError, match=msg):
168+
system.get_iam_diffuse(45, iam_model='not_a_model')
169+
170+
171+
def test_PVSystem_get_iam_diffuse_marion_invalid(sapm_module_params):
172+
system = pvsystem.PVSystem(module_parameters=sapm_module_params)
173+
msg = 'not a valid IAM model'
174+
with pytest.raises(ValueError, match=msg):
175+
system.get_iam_diffuse(45, iam_model='marion_diffuse',
176+
marion_model='not_a_model')
177+
178+
179+
def test_PVSystem_get_iam_diffuse_marion_missing_model(sapm_module_params):
180+
system = pvsystem.PVSystem(module_parameters=sapm_module_params)
181+
with pytest.raises(ValueError, match="marion_model must be specified"):
182+
system.get_iam_diffuse(45, iam_model='marion_diffuse')
183+
184+
115185
def test_retrieve_sam_raises_exceptions():
116186
"""
117187
Raise an exception if an invalid parameter is provided to `retrieve_sam()`.

0 commit comments

Comments
 (0)