Skip to content

Commit a349bea

Browse files
adriessewholmgren
authored andcommitted
Implement IEC 61853 IAM calculations for diffuse irradiance (#793)
* Functional new function. * Undo * Working function; partial docstring; no tests. * Complete docstring; simplify a bit; start tests. * Getting better all the time! * Adjust some comments and update whatsnew. * Improvements as per review. * More changes as requested. * Fun with stickler. * Docstring corrections. * Remove all controversial code.
1 parent 945c339 commit a349bea

File tree

4 files changed

+157
-6
lines changed

4 files changed

+157
-6
lines changed

docs/sphinx/source/api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ Incident angle modifiers
220220
iam.physical
221221
iam.ashrae
222222
iam.martin_ruiz
223+
iam.martin_ruiz_diffuse
223224
iam.sapm
224225
iam.interp
225226

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ Enhancements
114114
* Add :py:func:`~pvlib.ivtools.fit_sdm_desoto`, a method to fit the De Soto single
115115
diode model to the typical specifications given in manufacturers datasheets.
116116
* Add `timeout` to :py:func:`pvlib.iotools.get_psm3`.
117+
* Created one new incidence angle modifier (IAM) function for diffuse irradiance:
118+
:py:func:`pvlib.iam.martin_ruiz_diffuse`. (:issue:`751`)
117119

118120
Bug fixes
119121
~~~~~~~~~

pvlib/iam.py

Lines changed: 109 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import pandas as pd
1313
from pvlib.tools import cosd, sind, tand, asind
1414

15-
1615
# a dict of required parameter names for each IAM model
1716
# keys are the function names for the IAM models
1817
IAM_MODEL_PARAMS = {
@@ -220,8 +219,8 @@ def martin_ruiz(aoi, a_r=0.16):
220219
-----
221220
`martin_ruiz` calculates the incidence angle modifier (IAM) as described in
222221
[1]. The information required is the incident angle (AOI) and the angular
223-
losses coefficient (a_r). Note that [1] has a corrigendum [2] which makes
224-
the document much simpler to understand.
222+
losses coefficient (a_r). Note that [1] has a corrigendum [2] which
223+
clarifies a mix-up of 'alpha's and 'a's in the former.
225224
226225
The incident angle modifier is defined as
227226
@@ -249,6 +248,7 @@ def martin_ruiz(aoi, a_r=0.16):
249248
250249
See Also
251250
--------
251+
iam.martin_ruiz_diffuse
252252
iam.physical
253253
iam.ashrae
254254
iam.interp
@@ -262,7 +262,7 @@ def martin_ruiz(aoi, a_r=0.16):
262262
a_r = np.asanyarray(a_r)
263263

264264
if np.any(np.less_equal(a_r, 0)):
265-
raise RuntimeError("The parameter 'a_r' cannot be zero or negative.")
265+
raise ValueError("The parameter 'a_r' cannot be zero or negative.")
266266

267267
with np.errstate(invalid='ignore'):
268268
iam = (1 - np.exp(-cosd(aoi) / a_r)) / (1 - np.exp(-1 / a_r))
@@ -274,6 +274,111 @@ def martin_ruiz(aoi, a_r=0.16):
274274
return iam
275275

276276

277+
def martin_ruiz_diffuse(surface_tilt, a_r=0.16, c1=0.4244, c2=None):
278+
'''
279+
Determine the incidence angle modifiers (iam) for diffuse sky and
280+
ground-reflected irradiance using the Martin and Ruiz incident angle model.
281+
282+
Parameters
283+
----------
284+
surface_tilt: float or array-like, default 0
285+
Surface tilt angles in decimal degrees.
286+
The tilt angle is defined as degrees from horizontal
287+
(e.g. surface facing up = 0, surface facing horizon = 90)
288+
surface_tilt must be in the range [0, 180]
289+
290+
a_r : numeric
291+
The angular losses coefficient described in equation 3 of [1].
292+
This is an empirical dimensionless parameter. Values of a_r are
293+
generally on the order of 0.08 to 0.25 for flat-plate PV modules.
294+
a_r must be greater than zero.
295+
296+
c1 : float
297+
First fitting parameter for the expressions that approximate the
298+
integral of diffuse irradiance coming from different directions.
299+
c1 is given as the constant 4 / 3 / pi (0.4244) in [1].
300+
301+
c2 : float
302+
Second fitting parameter for the expressions that approximate the
303+
integral of diffuse irradiance coming from different directions.
304+
If c2 is None, it will be calculated according to the linear
305+
relationship given in [3].
306+
307+
Returns
308+
-------
309+
iam_sky : numeric
310+
The incident angle modifier for sky diffuse
311+
312+
iam_ground : numeric
313+
The incident angle modifier for ground-reflected diffuse
314+
315+
Notes
316+
-----
317+
Sky and ground modifiers are complementary: iam_sky for tilt = 30 is
318+
equal to iam_ground for tilt = 180 - 30. For vertical surfaces,
319+
tilt = 90, the two factors are equal.
320+
321+
References
322+
----------
323+
[1] N. Martin and J. M. Ruiz, "Calculation of the PV modules angular
324+
losses under field conditions by means of an analytical model", Solar
325+
Energy Materials & Solar Cells, vol. 70, pp. 25-38, 2001.
326+
327+
[2] N. Martin and J. M. Ruiz, "Corrigendum to 'Calculation of the PV
328+
modules angular losses under field conditions by means of an
329+
analytical model'", Solar Energy Materials & Solar Cells, vol. 110,
330+
pp. 154, 2013.
331+
332+
[3] "IEC 61853-3 Photovoltaic (PV) module performance testing and energy
333+
rating - Part 3: Energy rating of PV modules". IEC, Geneva, 2018.
334+
335+
See Also
336+
--------
337+
iam.martin_ruiz
338+
iam.physical
339+
iam.ashrae
340+
iam.interp
341+
iam.sapm
342+
'''
343+
# Contributed by Anton Driesse (@adriesse), PV Performance Labs. Oct. 2019
344+
345+
if isinstance(surface_tilt, pd.Series):
346+
out_index = surface_tilt.index
347+
else:
348+
out_index = None
349+
350+
surface_tilt = np.asanyarray(surface_tilt)
351+
352+
# avoid undefined results for horizontal or upside-down surfaces
353+
zeroang = 1e-06
354+
355+
surface_tilt = np.where(surface_tilt == 0, zeroang, surface_tilt)
356+
surface_tilt = np.where(surface_tilt == 180, 180 - zeroang, surface_tilt)
357+
358+
if c2 is None:
359+
# This equation is from [3] Sect. 7.2
360+
c2 = 0.5 * a_r - 0.154
361+
362+
beta = np.radians(surface_tilt)
363+
364+
from numpy import pi, sin, cos, exp
365+
366+
# because sin(pi) isn't exactly zero
367+
sin_beta = np.where(surface_tilt < 90, sin(beta), sin(pi - beta))
368+
369+
trig_term_sky = sin_beta + (pi - beta - sin_beta) / (1 + cos(beta))
370+
trig_term_gnd = sin_beta + (beta - sin_beta) / (1 - cos(beta)) # noqa: E222 E261 E501
371+
372+
iam_sky = 1 - exp(-(c1 + c2 * trig_term_sky) * trig_term_sky / a_r)
373+
iam_gnd = 1 - exp(-(c1 + c2 * trig_term_gnd) * trig_term_gnd / a_r)
374+
375+
if out_index is not None:
376+
iam_sky = pd.Series(iam_sky, index=out_index, name='iam_sky')
377+
iam_gnd = pd.Series(iam_gnd, index=out_index, name='iam_ground')
378+
379+
return iam_sky, iam_gnd
380+
381+
277382
def interp(aoi, theta_ref, iam_ref, method='linear', normalize=True):
278383
r'''
279384
Determine the incidence angle modifier (IAM) by interpolating a set of

pvlib/test/test_iam.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def test_martin_ruiz():
7777
# will fail if default values change
7878
iam = _iam.martin_ruiz(aoi)
7979
assert_allclose(iam, expected)
80+
8081
# will fail if parameter names change
8182
iam = _iam.martin_ruiz(aoi=aoi, a_r=a_r)
8283
assert_allclose(iam, expected)
@@ -99,11 +100,53 @@ def test_martin_ruiz():
99100
iam = _iam.martin_ruiz(aoi, a_r)
100101
assert_series_equal(iam, expected)
101102

102-
# check exception clause
103-
with pytest.raises(RuntimeError):
103+
104+
def test_martin_ruiz_exception():
105+
106+
with pytest.raises(ValueError):
104107
_iam.martin_ruiz(0.0, a_r=0.0)
105108

106109

110+
def test_martin_ruiz_diffuse():
111+
112+
surface_tilt = 30.
113+
a_r = 0.16
114+
expected = (0.9549735, 0.7944426)
115+
116+
# will fail if default values change
117+
iam = _iam.martin_ruiz_diffuse(surface_tilt)
118+
assert_allclose(iam, expected)
119+
120+
# will fail if parameter names change
121+
iam = _iam.martin_ruiz_diffuse(surface_tilt=surface_tilt, a_r=a_r)
122+
assert_allclose(iam, expected)
123+
124+
a_r = 0.18
125+
surface_tilt = [0, 30, 90, 120, 180, np.nan, np.inf]
126+
expected_sky = [0.9407678, 0.9452250, 0.9407678, 0.9055541, 0.0000000,
127+
np.nan, np.nan]
128+
expected_gnd = [0.0000000, 0.7610849, 0.9407678, 0.9483508, 0.9407678,
129+
np.nan, np.nan]
130+
131+
# check various inputs as list
132+
iam = _iam.martin_ruiz_diffuse(surface_tilt, a_r)
133+
assert_allclose(iam[0], expected_sky, atol=1e-7, equal_nan=True)
134+
assert_allclose(iam[1], expected_gnd, atol=1e-7, equal_nan=True)
135+
136+
# check various inputs as array
137+
iam = _iam.martin_ruiz_diffuse(np.array(surface_tilt), a_r)
138+
assert_allclose(iam[0], expected_sky, atol=1e-7, equal_nan=True)
139+
assert_allclose(iam[1], expected_gnd, atol=1e-7, equal_nan=True)
140+
141+
# check various inputs as Series
142+
surface_tilt = pd.Series(surface_tilt)
143+
expected_sky = pd.Series(expected_sky, name='iam_sky')
144+
expected_gnd = pd.Series(expected_gnd, name='iam_ground')
145+
iam = _iam.martin_ruiz_diffuse(surface_tilt, a_r)
146+
assert_series_equal(iam[0], expected_sky)
147+
assert_series_equal(iam[1], expected_gnd)
148+
149+
107150
@requires_scipy
108151
def test_iam_interp():
109152

0 commit comments

Comments
 (0)