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 17 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 @@ -180,6 +180,7 @@ Utilities for working with IV curve data
:toctree: generated/

ivtools.utils.rectify_iv_curve
ivtools.params.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 @@ -18,6 +18,8 @@ Enhancements
(:issue:`1565`, :pull:`1567`)
* Add optional ``return_components`` parameter to :py:func:`pvlib.irradiance.haydavies` to return
individual diffuse irradiance components (:issue:`1553`, :pull:`1568`)
* Add a funtion ``pvlib.ivtools.params.astm_e1036()`` to perform ASTM E1036 extraction of IV
curve parameters (:pull:`1585`)


Bug fixes
Expand Down Expand Up @@ -51,6 +53,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
2 changes: 1 addition & 1 deletion pvlib/ivtools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

"""

from pvlib.ivtools import sde, sdm, utils # noqa: F401
from pvlib.ivtools import sde, sdm, utils, params # noqa: F401
133 changes: 133 additions & 0 deletions pvlib/ivtools/params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""
The ``params`` module contains classes and functions to extract parameters
(e.g. Isc and Voc) from current-voltage curves.
"""
import pandas as pd
import numpy as np
from numpy.polynomial.polynomial import Polynomial as Poly


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 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=4 : 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()
Copy link
Member

@adriesse adriesse Nov 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think everything could all be calculated with numpy only without many changes, if that is of interest.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting suggestion. I don't think I will have time to tackle it, but if you feel it makes a sufficient improvement please go for it. Tests are in place now so it should be easy to verify that it has worked.

df['v'] = v
df['i'] = i
df['p'] = df['v'] * df['i']

# first calculate estimates of voc and isc
voc = np.nan
isc = np.nan

# 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 abs(df['i'][i_min_ind]) <= isc_est * 0.001:
voc = voc_est
if abs(df['v'][v_min_ind]) <= voc_est * 0.005:
isc = isc_est

# perform a linear fit if estimates rejected
if np.isnan(voc):
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 np.isnan(isc):
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: 97 additions & 0 deletions pvlib/tests/ivtools/test_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import numpy as np
import pytest
from pvlib.ivtools import params


@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


def test_astm_e1036(v_array, i_array):
result = params.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 = params.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 = params.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 = params.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 = params.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)