Skip to content

add ASTM E1036 parameter extraction #1585

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Dec 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/sphinx/source/reference/pv_modeling.rst
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ Utilities for working with IV curve data
:toctree: generated/

ivtools.utils.rectify_iv_curve
ivtools.utils.astm_e1036

Other
-----
Expand Down
3 changes: 3 additions & 0 deletions docs/sphinx/source/whatsnew/v0.9.4.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ Enhancements
in a simplified way, using the Faiman model as an example.
:py:func:`~pvlib.temperature.faiman_rad`
(:issue:`1594`, :pull:`1595`)
* Add a function :py:func:`pvlib.ivtools.utils.astm_e1036` to perform ASTM E1036 extraction of IV
curve parameters (:pull:`1585`)

Bug fixes
~~~~~~~~~
Expand Down Expand Up @@ -78,6 +80,7 @@ Contributors
* Christian Orner (:ghuser:`chrisorner`)
* Saurabh Aneja (:ghuser:`spaneja`)
* Marcus Boumans (:ghuser:`bowie2211`)
* Michael Deceglie (:ghuser:`mdeceglie`)
* Yu Xie (:ghuser:`xieyupku`)
* Anton Driesse (:ghuser:`adriesse`)
* Cliff Hansen (:ghuser:`cwhanse`)
Expand Down
121 changes: 121 additions & 0 deletions pvlib/ivtools/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import numpy as np
import pandas as pd
from numpy.polynomial.polynomial import Polynomial as Poly


# A small number used to decide when a slope is equivalent to zero
Expand Down Expand Up @@ -423,3 +424,123 @@ def _schumaker_qspline(x, y):
yhat = tmp2[:, 4]
kflag = tmp2[:, 5]
return t, c, yhat, kflag


def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15),
voc_points=3, isc_points=3, mp_fit_order=4):
'''
Extract photovoltaic IV parameters according to ASTM E1036. Assumes that
the power producing portion of the curve is in the first quadrant.

Parameters
----------
v : array-like
Voltage points
i : array-like
Current points
imax_limits : tuple, default (0.75, 1.15)
Two-element tuple (low, high) specifying the fraction of estimated
Imp within which to fit a polynomial for max power calculation
vmax_limits : tuple, default (0.75, 1.15)
Two-element tuple (low, high) specifying the fraction of estimated
Vmp within which to fit a polynomial for max power calculation
voc_points : int, default 3
The number of points near open circuit to use for linear fit
and Voc calculation
isc_points : int, default 3
The number of points near short circuit to use for linear fit and
Isc calculation
mp_fit_order : int, default 4
The order of the polynomial fit of power vs. voltage near maximum
power


Returns
-------
dict
Results. The IV parameters are given by the keys 'voc', 'isc',
'vmp', 'imp', 'pmp', and 'ff'. The key 'mp_fit' gives the numpy
Polynomial object for the fit of power vs voltage near maximum
power.

References
----------
.. [1] Standard Test Methods for Electrical Performance of Nonconcentrator
Terrestrial Photovoltaic Modules and Arrays Using Reference Cells,
ASTM E1036-15(2019), :doi:`10.1520/E1036-15R19`
'''

# Adapted from https://github.com/NREL/iv_params
# Copyright (c) 2022, Alliance for Sustainable Energy, LLC
# All rights reserved.

df = pd.DataFrame()
df['v'] = v
df['i'] = i
df['p'] = df['v'] * df['i']

# determine if we can use voc and isc estimates
i_min_ind = df['i'].abs().idxmin()
v_min_ind = df['v'].abs().idxmin()
voc_est = df['v'][i_min_ind]
isc_est = df['i'][v_min_ind]

# accept the estimates if they are close enough
# if not, perform a linear fit
if abs(df['i'][i_min_ind]) <= isc_est * 0.001:
voc = voc_est
else:
df['i_abs'] = df['i'].abs()
voc_df = df.nsmallest(voc_points, 'i_abs')
voc_fit = Poly.fit(voc_df['i'], voc_df['v'], 1)
voc = voc_fit(0)

if abs(df['v'][v_min_ind]) <= voc_est * 0.005:
isc = isc_est
else:
df['v_abs'] = df['v'].abs()
isc_df = df.nsmallest(isc_points, 'v_abs')
isc_fit = Poly.fit(isc_df['v'], isc_df['i'], 1)
isc = isc_fit(0)

# estimate max power point
max_index = df['p'].idxmax()
mp_est = df.loc[max_index]

# filter around max power
mask = (
(df['i'] >= imax_limits[0] * mp_est['i']) &
(df['i'] <= imax_limits[1] * mp_est['i']) &
(df['v'] >= vmax_limits[0] * mp_est['v']) &
(df['v'] <= vmax_limits[1] * mp_est['v'])
)
filtered = df[mask]

# fit polynomial and find max
mp_fit = Poly.fit(filtered['v'], filtered['p'], mp_fit_order)
# Note that this root finding procedure differs from
# the suggestion in the standard
roots = mp_fit.deriv().roots()
# only consider real roots
roots = roots.real[abs(roots.imag) < 1e-5]
# only consider roots in the relevant part of the domain
roots = roots[(roots < filtered['v'].max()) &
(roots > filtered['v'].min())]
vmp = roots[np.argmax(mp_fit(roots))]
pmp = mp_fit(vmp)
# Imp isn't mentioned for update in the
# standard, but this seems to be in the intended spirit
imp = pmp / vmp

ff = pmp / (voc * isc)

result = {}
result['voc'] = voc
result['isc'] = isc
result['vmp'] = vmp
result['imp'] = imp
result['pmp'] = pmp
result['ff'] = ff
result['mp_fit'] = mp_fit

return result
97 changes: 96 additions & 1 deletion pvlib/tests/ivtools/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import numpy as np
import pandas as pd
import pytest
from pvlib.ivtools.utils import _numdiff, rectify_iv_curve
from pvlib.ivtools.utils import _numdiff, rectify_iv_curve, astm_e1036
from pvlib.ivtools.utils import _schumaker_qspline

from ..conftest import DATA_DIR
Expand Down Expand Up @@ -76,3 +76,98 @@ def test__schmumaker_qspline(x, y, expected):
np.testing.assert_allclose(t, expected[1], atol=0.0001)
np.testing.assert_allclose(yhat, expected[2], atol=0.0001)
np.testing.assert_allclose(kflag, expected[3], atol=0.0001)


@pytest.fixture
def i_array():
i = np.array([8.09403993, 8.09382549, 8.09361103, 8.09339656, 8.09318205,
8.09296748, 8.09275275, 8.09253771, 8.09232204, 8.09210506,
8.09188538, 8.09166014, 8.09142342, 8.09116305, 8.09085392,
8.09044425, 8.08982734, 8.08878333, 8.08685945, 8.08312463,
8.07566926, 8.06059856, 8.03005836, 7.96856869, 7.8469714,
7.61489584, 7.19789314, 6.51138396, 5.49373476, 4.13267172,
2.46021487, 0.52838624, -1.61055289])
return i


@pytest.fixture
def v_array():
v = np.array([-0.005, 0.015, 0.035, 0.055, 0.075, 0.095, 0.115, 0.135,
0.155, 0.175, 0.195, 0.215, 0.235, 0.255, 0.275, 0.295,
0.315, 0.335, 0.355, 0.375, 0.395, 0.415, 0.435, 0.455,
0.475, 0.495, 0.515, 0.535, 0.555, 0.575, 0.595, 0.615,
0.635])
return v


# astm_e1036 tests
def test_astm_e1036(v_array, i_array):
result = astm_e1036(v_array, i_array)
expected = {'voc': 0.6195097477985162,
'isc': 8.093986320386227,
'vmp': 0.494283417170082,
'imp': 7.626088301548568,
'pmp': 3.7694489853302127,
'ff': 0.7517393078504361}
fit = result.pop('mp_fit')
expected_fit = np.array(
[3.6260726, 0.49124176, -0.24644747, -0.26442383, -0.1223237])
assert fit.coef == pytest.approx(expected_fit)
assert result == pytest.approx(expected)


def test_astm_e1036_fit_order(v_array, i_array):
result = astm_e1036(v_array, i_array, mp_fit_order=3)
fit = result.pop('mp_fit')
expected_fit = np.array(
[3.64081697, 0.49124176, -0.3720477, -0.26442383])
assert fit.coef == pytest.approx(expected_fit)


def test_astm_e1036_est_isc_voc(v_array, i_array):
'''
Test the case in which Isc and Voc estimates are
valid without a linear fit
'''
v = v_array
i = i_array
v = np.append(v, [0.001, 0.6201])
i = np.append(i, [8.09397560e+00, 7.10653445e-04])
result = astm_e1036(v, i)
expected = {'voc': 0.6201,
'isc': 8.093975598317805,
'vmp': 0.494283417170082,
'imp': 7.626088301548568,
'pmp': 3.7694489853302127,
'ff': 0.751024747526615}
result.pop('mp_fit')
assert result == pytest.approx(expected)


def test_astm_e1036_mpfit_limits(v_array, i_array):
result = astm_e1036(v_array,
i_array,
imax_limits=(0.85, 1.1),
vmax_limits=(0.85, 1.1))
expected = {'voc': 0.6195097477985162,
'isc': 8.093986320386227,
'vmp': 0.49464214190725303,
'imp': 7.620032530519718,
'pmp': 3.769189212299219,
'ff': 0.7516875014460312}
result.pop('mp_fit')
assert result == pytest.approx(expected)


def test_astm_e1036_fit_points(v_array, i_array):
i = i_array
i[3] = 8.1 # ensure an interesting change happens
result = astm_e1036(v_array, i, voc_points=4, isc_points=4)
expected = {'voc': 0.619337073271274,
'isc': 8.093160893325297,
'vmp': 0.494283417170082,
'imp': 7.626088301548568,
'pmp': 3.7694489853302127,
'ff': 0.7520255886236707}
result.pop('mp_fit')
assert result == pytest.approx(expected)