Skip to content

Commit a5f3ef4

Browse files
add ASTM E1036 parameter extraction (#1585)
* add astm_e1036 function * add newline to end of file * typo fix Co-authored-by: Kevin Anderson <[email protected]> * add tests * fix assert statments * add defaults to docstring * specify the dict keys * add note regarding root finding * add note re: Imp * fix capitalization * Add reference * return the polynomial fit object * capitalization * add polynomial fit order parameter * add entry to pv_modeling.rst * Update whats new * fix docstring Co-authored-by: Kevin Anderson <[email protected]> * update reference Co-authored-by: Kevin Anderson <[email protected]> * update whatsnew Co-authored-by: Kevin Anderson <[email protected]> * update module description Co-authored-by: Kevin Anderson <[email protected]> * adjust first quadrant explanation * Move to utils modules * Simplify logic * move copyright statement to a comment Co-authored-by: Kevin Anderson <[email protected]>
1 parent e6e5b04 commit a5f3ef4

File tree

4 files changed

+221
-1
lines changed

4 files changed

+221
-1
lines changed

docs/sphinx/source/reference/pv_modeling.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ Utilities for working with IV curve data
186186
:toctree: generated/
187187

188188
ivtools.utils.rectify_iv_curve
189+
ivtools.utils.astm_e1036
189190

190191
Other
191192
-----

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ Enhancements
4545
in a simplified way, using the Faiman model as an example.
4646
:py:func:`~pvlib.temperature.faiman_rad`
4747
(:issue:`1594`, :pull:`1595`)
48+
* Add a function :py:func:`pvlib.ivtools.utils.astm_e1036` to perform ASTM E1036 extraction of IV
49+
curve parameters (:pull:`1585`)
4850

4951
Bug fixes
5052
~~~~~~~~~
@@ -78,6 +80,7 @@ Contributors
7880
* Christian Orner (:ghuser:`chrisorner`)
7981
* Saurabh Aneja (:ghuser:`spaneja`)
8082
* Marcus Boumans (:ghuser:`bowie2211`)
83+
* Michael Deceglie (:ghuser:`mdeceglie`)
8184
* Yu Xie (:ghuser:`xieyupku`)
8285
* Anton Driesse (:ghuser:`adriesse`)
8386
* Cliff Hansen (:ghuser:`cwhanse`)

pvlib/ivtools/utils.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import numpy as np
88
import pandas as pd
9+
from numpy.polynomial.polynomial import Polynomial as Poly
910

1011

1112
# A small number used to decide when a slope is equivalent to zero
@@ -423,3 +424,123 @@ def _schumaker_qspline(x, y):
423424
yhat = tmp2[:, 4]
424425
kflag = tmp2[:, 5]
425426
return t, c, yhat, kflag
427+
428+
429+
def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15),
430+
voc_points=3, isc_points=3, mp_fit_order=4):
431+
'''
432+
Extract photovoltaic IV parameters according to ASTM E1036. Assumes that
433+
the power producing portion of the curve is in the first quadrant.
434+
435+
Parameters
436+
----------
437+
v : array-like
438+
Voltage points
439+
i : array-like
440+
Current points
441+
imax_limits : tuple, default (0.75, 1.15)
442+
Two-element tuple (low, high) specifying the fraction of estimated
443+
Imp within which to fit a polynomial for max power calculation
444+
vmax_limits : tuple, default (0.75, 1.15)
445+
Two-element tuple (low, high) specifying the fraction of estimated
446+
Vmp within which to fit a polynomial for max power calculation
447+
voc_points : int, default 3
448+
The number of points near open circuit to use for linear fit
449+
and Voc calculation
450+
isc_points : int, default 3
451+
The number of points near short circuit to use for linear fit and
452+
Isc calculation
453+
mp_fit_order : int, default 4
454+
The order of the polynomial fit of power vs. voltage near maximum
455+
power
456+
457+
458+
Returns
459+
-------
460+
dict
461+
Results. The IV parameters are given by the keys 'voc', 'isc',
462+
'vmp', 'imp', 'pmp', and 'ff'. The key 'mp_fit' gives the numpy
463+
Polynomial object for the fit of power vs voltage near maximum
464+
power.
465+
466+
References
467+
----------
468+
.. [1] Standard Test Methods for Electrical Performance of Nonconcentrator
469+
Terrestrial Photovoltaic Modules and Arrays Using Reference Cells,
470+
ASTM E1036-15(2019), :doi:`10.1520/E1036-15R19`
471+
'''
472+
473+
# Adapted from https://github.com/NREL/iv_params
474+
# Copyright (c) 2022, Alliance for Sustainable Energy, LLC
475+
# All rights reserved.
476+
477+
df = pd.DataFrame()
478+
df['v'] = v
479+
df['i'] = i
480+
df['p'] = df['v'] * df['i']
481+
482+
# determine if we can use voc and isc estimates
483+
i_min_ind = df['i'].abs().idxmin()
484+
v_min_ind = df['v'].abs().idxmin()
485+
voc_est = df['v'][i_min_ind]
486+
isc_est = df['i'][v_min_ind]
487+
488+
# accept the estimates if they are close enough
489+
# if not, perform a linear fit
490+
if abs(df['i'][i_min_ind]) <= isc_est * 0.001:
491+
voc = voc_est
492+
else:
493+
df['i_abs'] = df['i'].abs()
494+
voc_df = df.nsmallest(voc_points, 'i_abs')
495+
voc_fit = Poly.fit(voc_df['i'], voc_df['v'], 1)
496+
voc = voc_fit(0)
497+
498+
if abs(df['v'][v_min_ind]) <= voc_est * 0.005:
499+
isc = isc_est
500+
else:
501+
df['v_abs'] = df['v'].abs()
502+
isc_df = df.nsmallest(isc_points, 'v_abs')
503+
isc_fit = Poly.fit(isc_df['v'], isc_df['i'], 1)
504+
isc = isc_fit(0)
505+
506+
# estimate max power point
507+
max_index = df['p'].idxmax()
508+
mp_est = df.loc[max_index]
509+
510+
# filter around max power
511+
mask = (
512+
(df['i'] >= imax_limits[0] * mp_est['i']) &
513+
(df['i'] <= imax_limits[1] * mp_est['i']) &
514+
(df['v'] >= vmax_limits[0] * mp_est['v']) &
515+
(df['v'] <= vmax_limits[1] * mp_est['v'])
516+
)
517+
filtered = df[mask]
518+
519+
# fit polynomial and find max
520+
mp_fit = Poly.fit(filtered['v'], filtered['p'], mp_fit_order)
521+
# Note that this root finding procedure differs from
522+
# the suggestion in the standard
523+
roots = mp_fit.deriv().roots()
524+
# only consider real roots
525+
roots = roots.real[abs(roots.imag) < 1e-5]
526+
# only consider roots in the relevant part of the domain
527+
roots = roots[(roots < filtered['v'].max()) &
528+
(roots > filtered['v'].min())]
529+
vmp = roots[np.argmax(mp_fit(roots))]
530+
pmp = mp_fit(vmp)
531+
# Imp isn't mentioned for update in the
532+
# standard, but this seems to be in the intended spirit
533+
imp = pmp / vmp
534+
535+
ff = pmp / (voc * isc)
536+
537+
result = {}
538+
result['voc'] = voc
539+
result['isc'] = isc
540+
result['vmp'] = vmp
541+
result['imp'] = imp
542+
result['pmp'] = pmp
543+
result['ff'] = ff
544+
result['mp_fit'] = mp_fit
545+
546+
return result

pvlib/tests/ivtools/test_utils.py

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import numpy as np
22
import pandas as pd
33
import pytest
4-
from pvlib.ivtools.utils import _numdiff, rectify_iv_curve
4+
from pvlib.ivtools.utils import _numdiff, rectify_iv_curve, astm_e1036
55
from pvlib.ivtools.utils import _schumaker_qspline
66

77
from ..conftest import DATA_DIR
@@ -76,3 +76,98 @@ def test__schmumaker_qspline(x, y, expected):
7676
np.testing.assert_allclose(t, expected[1], atol=0.0001)
7777
np.testing.assert_allclose(yhat, expected[2], atol=0.0001)
7878
np.testing.assert_allclose(kflag, expected[3], atol=0.0001)
79+
80+
81+
@pytest.fixture
82+
def i_array():
83+
i = np.array([8.09403993, 8.09382549, 8.09361103, 8.09339656, 8.09318205,
84+
8.09296748, 8.09275275, 8.09253771, 8.09232204, 8.09210506,
85+
8.09188538, 8.09166014, 8.09142342, 8.09116305, 8.09085392,
86+
8.09044425, 8.08982734, 8.08878333, 8.08685945, 8.08312463,
87+
8.07566926, 8.06059856, 8.03005836, 7.96856869, 7.8469714,
88+
7.61489584, 7.19789314, 6.51138396, 5.49373476, 4.13267172,
89+
2.46021487, 0.52838624, -1.61055289])
90+
return i
91+
92+
93+
@pytest.fixture
94+
def v_array():
95+
v = np.array([-0.005, 0.015, 0.035, 0.055, 0.075, 0.095, 0.115, 0.135,
96+
0.155, 0.175, 0.195, 0.215, 0.235, 0.255, 0.275, 0.295,
97+
0.315, 0.335, 0.355, 0.375, 0.395, 0.415, 0.435, 0.455,
98+
0.475, 0.495, 0.515, 0.535, 0.555, 0.575, 0.595, 0.615,
99+
0.635])
100+
return v
101+
102+
103+
# astm_e1036 tests
104+
def test_astm_e1036(v_array, i_array):
105+
result = astm_e1036(v_array, i_array)
106+
expected = {'voc': 0.6195097477985162,
107+
'isc': 8.093986320386227,
108+
'vmp': 0.494283417170082,
109+
'imp': 7.626088301548568,
110+
'pmp': 3.7694489853302127,
111+
'ff': 0.7517393078504361}
112+
fit = result.pop('mp_fit')
113+
expected_fit = np.array(
114+
[3.6260726, 0.49124176, -0.24644747, -0.26442383, -0.1223237])
115+
assert fit.coef == pytest.approx(expected_fit)
116+
assert result == pytest.approx(expected)
117+
118+
119+
def test_astm_e1036_fit_order(v_array, i_array):
120+
result = astm_e1036(v_array, i_array, mp_fit_order=3)
121+
fit = result.pop('mp_fit')
122+
expected_fit = np.array(
123+
[3.64081697, 0.49124176, -0.3720477, -0.26442383])
124+
assert fit.coef == pytest.approx(expected_fit)
125+
126+
127+
def test_astm_e1036_est_isc_voc(v_array, i_array):
128+
'''
129+
Test the case in which Isc and Voc estimates are
130+
valid without a linear fit
131+
'''
132+
v = v_array
133+
i = i_array
134+
v = np.append(v, [0.001, 0.6201])
135+
i = np.append(i, [8.09397560e+00, 7.10653445e-04])
136+
result = astm_e1036(v, i)
137+
expected = {'voc': 0.6201,
138+
'isc': 8.093975598317805,
139+
'vmp': 0.494283417170082,
140+
'imp': 7.626088301548568,
141+
'pmp': 3.7694489853302127,
142+
'ff': 0.751024747526615}
143+
result.pop('mp_fit')
144+
assert result == pytest.approx(expected)
145+
146+
147+
def test_astm_e1036_mpfit_limits(v_array, i_array):
148+
result = astm_e1036(v_array,
149+
i_array,
150+
imax_limits=(0.85, 1.1),
151+
vmax_limits=(0.85, 1.1))
152+
expected = {'voc': 0.6195097477985162,
153+
'isc': 8.093986320386227,
154+
'vmp': 0.49464214190725303,
155+
'imp': 7.620032530519718,
156+
'pmp': 3.769189212299219,
157+
'ff': 0.7516875014460312}
158+
result.pop('mp_fit')
159+
assert result == pytest.approx(expected)
160+
161+
162+
def test_astm_e1036_fit_points(v_array, i_array):
163+
i = i_array
164+
i[3] = 8.1 # ensure an interesting change happens
165+
result = astm_e1036(v_array, i, voc_points=4, isc_points=4)
166+
expected = {'voc': 0.619337073271274,
167+
'isc': 8.093160893325297,
168+
'vmp': 0.494283417170082,
169+
'imp': 7.626088301548568,
170+
'pmp': 3.7694489853302127,
171+
'ff': 0.7520255886236707}
172+
result.pop('mp_fit')
173+
assert result == pytest.approx(expected)

0 commit comments

Comments
 (0)