-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Changes from 17 commits
37dc1e9
1d07966
a403a66
91d7f91
5beca77
4926f80
c9647cd
52435dc
7f80cab
8d15aad
339522c
2994521
0955c7c
ef969f3
f7d6faf
967d394
0e427ea
1dface6
a817355
f88e665
2393b84
7294dfd
f3a0e18
25ababf
3f306cc
d922374
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
""" | ||
The ``params`` module contains classes and functions to extract parameters | ||
mdeceglie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
(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 | ||
kandersolar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 | ||
mdeceglie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
mdeceglie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
mdeceglie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Adapted from https://github.com/NREL/iv_params | ||
Copyright (c) 2022, Alliance for Sustainable Energy, LLC | ||
All rights reserved. | ||
''' | ||
|
||
df = pd.DataFrame() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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))] | ||
mdeceglie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
mdeceglie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
ff = pmp / (voc * isc) | ||
|
||
result = {} | ||
result['voc'] = voc | ||
result['isc'] = isc | ||
result['vmp'] = vmp | ||
result['imp'] = imp | ||
result['pmp'] = pmp | ||
result['ff'] = ff | ||
mdeceglie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
result['mp_fit'] = mp_fit | ||
|
||
return result |
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) |
Uh oh!
There was an error while loading. Please reload this page.