From 37dc1e97facdb2f430fc43ccb48dfc6cd8f1d414 Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Wed, 2 Nov 2022 13:46:32 -0600 Subject: [PATCH 01/24] add astm_e1036 function --- pvlib/ivtools/__init__.py | 2 +- pvlib/ivtools/params.py | 114 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 pvlib/ivtools/params.py diff --git a/pvlib/ivtools/__init__.py b/pvlib/ivtools/__init__.py index de897e2989..565380dd22 100644 --- a/pvlib/ivtools/__init__.py +++ b/pvlib/ivtools/__init__.py @@ -4,4 +4,4 @@ """ -from pvlib.ivtools import sde, sdm, utils # noqa: F401 +from pvlib.ivtools import sde, sdm, utils, params # noqa: F401 diff --git a/pvlib/ivtools/params.py b/pvlib/ivtools/params.py new file mode 100644 index 0000000000..87c39e15da --- /dev/null +++ b/pvlib/ivtools/params.py @@ -0,0 +1,114 @@ +""" +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): + ''' + 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 + Two-element tuple (low, high) specifying the fraction of estimated + Imp within which to fit a polynomial for max power calculation + vmax_limits : tuple + Two-element tuple (low, high) specifying the fraction of estimated + Vmp within which to fit a polynomial for max power calculation + voc_points : int + the number of points near open circuit to use for linear fit + and Voc calculation + isc_points : int + the number of points near short circuit to use for linear fit and + Isc calculation + + Returns + ------- + dict + Calculated IV parameters + + 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'] + + # 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'], 4) + roots = mp_fit.deriv().roots() + # only coniser 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 = 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 + + return result \ No newline at end of file From 1d07966ae0c19519aec7e5bc7937825f3656f12d Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Wed, 2 Nov 2022 13:58:07 -0600 Subject: [PATCH 02/24] add newline to end of file --- pvlib/ivtools/params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/ivtools/params.py b/pvlib/ivtools/params.py index 87c39e15da..359272e195 100644 --- a/pvlib/ivtools/params.py +++ b/pvlib/ivtools/params.py @@ -111,4 +111,4 @@ def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15), result['pmp'] = pmp result['ff'] = ff - return result \ No newline at end of file + return result From a403a66d35b82d97a61549d5caca14b348b78511 Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Mon, 21 Nov 2022 13:49:47 -0700 Subject: [PATCH 03/24] typo fix Co-authored-by: Kevin Anderson --- pvlib/ivtools/params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/ivtools/params.py b/pvlib/ivtools/params.py index 359272e195..210526687d 100644 --- a/pvlib/ivtools/params.py +++ b/pvlib/ivtools/params.py @@ -92,7 +92,7 @@ def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15), # fit polynomial and find max mp_fit = Poly.fit(filtered['v'], filtered['p'], 4) roots = mp_fit.deriv().roots() - # only coniser real 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()) & From 91d7f916c2999d51986438c1f655e98676dcf015 Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Mon, 5 Dec 2022 12:52:04 -0700 Subject: [PATCH 04/24] add tests --- pvlib/tests/ivtools/test_params.py | 82 ++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 pvlib/tests/ivtools/test_params.py diff --git a/pvlib/tests/ivtools/test_params.py b/pvlib/tests/ivtools/test_params.py new file mode 100644 index 0000000000..e2340baf5b --- /dev/null +++ b/pvlib/tests/ivtools/test_params.py @@ -0,0 +1,82 @@ +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} + assert(result == pytest.approx(expected)) + + +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} + 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} + 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} + assert(result == pytest.approx(expected)) From 5beca77d05b043bbae49c4eb8f1e8280798d3840 Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Mon, 5 Dec 2022 13:35:50 -0700 Subject: [PATCH 05/24] fix assert statments --- pvlib/tests/ivtools/test_params.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pvlib/tests/ivtools/test_params.py b/pvlib/tests/ivtools/test_params.py index e2340baf5b..a6d073a260 100644 --- a/pvlib/tests/ivtools/test_params.py +++ b/pvlib/tests/ivtools/test_params.py @@ -33,7 +33,7 @@ def test_astm_e1036(v_array, i_array): 'imp': 7.626088301548568, 'pmp': 3.7694489853302127, 'ff': 0.7517393078504361} - assert(result == pytest.approx(expected)) + assert result == pytest.approx(expected) def test_astm_e1036_est_isc_voc(v_array, i_array): @@ -52,7 +52,7 @@ def test_astm_e1036_est_isc_voc(v_array, i_array): 'imp': 7.626088301548568, 'pmp': 3.7694489853302127, 'ff': 0.751024747526615} - assert(result == pytest.approx(expected)) + assert result == pytest.approx(expected) def test_astm_e1036_mpfit_limits(v_array, i_array): @@ -66,7 +66,7 @@ def test_astm_e1036_mpfit_limits(v_array, i_array): 'imp': 7.620032530519718, 'pmp': 3.769189212299219, 'ff': 0.7516875014460312} - assert(result == pytest.approx(expected)) + assert result == pytest.approx(expected) def test_astm_e1036_fit_points(v_array, i_array): @@ -79,4 +79,4 @@ def test_astm_e1036_fit_points(v_array, i_array): 'imp': 7.626088301548568, 'pmp': 3.7694489853302127, 'ff': 0.7520255886236707} - assert(result == pytest.approx(expected)) + assert result == pytest.approx(expected) From 4926f80cbdafd87f11053a369f01f6b5fc199e31 Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Mon, 5 Dec 2022 13:39:38 -0700 Subject: [PATCH 06/24] add defaults to docstring --- pvlib/ivtools/params.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pvlib/ivtools/params.py b/pvlib/ivtools/params.py index 210526687d..05beaa8e70 100644 --- a/pvlib/ivtools/params.py +++ b/pvlib/ivtools/params.py @@ -19,16 +19,16 @@ def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15), Voltage points i : array-like Current points - imax_limits : tuple + 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 + 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 + voc_points : int, default 3 the number of points near open circuit to use for linear fit and Voc calculation - isc_points : int + isc_points : int, default 3 the number of points near short circuit to use for linear fit and Isc calculation From c9647cdd877e0b60be6844447185dedf1a6077b5 Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Mon, 5 Dec 2022 13:46:49 -0700 Subject: [PATCH 07/24] specify the dict keys --- pvlib/ivtools/params.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pvlib/ivtools/params.py b/pvlib/ivtools/params.py index 05beaa8e70..74cbff1ca2 100644 --- a/pvlib/ivtools/params.py +++ b/pvlib/ivtools/params.py @@ -35,7 +35,8 @@ def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15), Returns ------- dict - Calculated IV parameters + Calculated IV parameters. Keys are 'voc', 'isc', 'vpm', 'imp', 'pmp', + and 'ff'. Adapted from https://github.com/NREL/iv_params Copyright (c) 2022, Alliance for Sustainable Energy, LLC From 52435dce927300f82dfa4781d6a3e47636ef3f0a Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Mon, 5 Dec 2022 13:48:23 -0700 Subject: [PATCH 08/24] add note regarding root finding --- pvlib/ivtools/params.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pvlib/ivtools/params.py b/pvlib/ivtools/params.py index 74cbff1ca2..0c7f7870fd 100644 --- a/pvlib/ivtools/params.py +++ b/pvlib/ivtools/params.py @@ -92,6 +92,8 @@ def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15), # fit polynomial and find max mp_fit = Poly.fit(filtered['v'], filtered['p'], 4) + # 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] From 7f80cabaa01252482a14637849061f28939faad6 Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Mon, 5 Dec 2022 13:50:03 -0700 Subject: [PATCH 09/24] add note re: Imp --- pvlib/ivtools/params.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pvlib/ivtools/params.py b/pvlib/ivtools/params.py index 0c7f7870fd..262bc32eda 100644 --- a/pvlib/ivtools/params.py +++ b/pvlib/ivtools/params.py @@ -102,6 +102,8 @@ def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15), (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) From 8d15aaddb9f2c62094cda3a10d543e58a30f0c59 Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Mon, 5 Dec 2022 13:53:54 -0700 Subject: [PATCH 10/24] fix capitalization --- pvlib/ivtools/params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/ivtools/params.py b/pvlib/ivtools/params.py index 262bc32eda..d8a72195fc 100644 --- a/pvlib/ivtools/params.py +++ b/pvlib/ivtools/params.py @@ -102,7 +102,7 @@ def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15), (roots > filtered['v'].min())] vmp = roots[np.argmax(mp_fit(roots))] pmp = mp_fit(vmp) - # imp isn't mentioned for update in the + # Imp isn't mentioned for update in the # standard, but this seems to be in the intended spirit imp = pmp / vmp From 339522c8d4517b73cc94db794eef06365bcac09b Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Mon, 5 Dec 2022 13:54:07 -0700 Subject: [PATCH 11/24] Add reference --- pvlib/ivtools/params.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pvlib/ivtools/params.py b/pvlib/ivtools/params.py index d8a72195fc..cd5397812b 100644 --- a/pvlib/ivtools/params.py +++ b/pvlib/ivtools/params.py @@ -38,6 +38,13 @@ def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15), Calculated IV parameters. Keys are 'voc', 'isc', 'vpm', 'imp', 'pmp', and 'ff'. + 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. From 2994521edd4b9a72b39ef6da008dd71a6d71ff01 Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Mon, 5 Dec 2022 14:40:44 -0700 Subject: [PATCH 12/24] return the polynomial fit object --- pvlib/ivtools/params.py | 7 +++++-- pvlib/tests/ivtools/test_params.py | 7 +++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pvlib/ivtools/params.py b/pvlib/ivtools/params.py index cd5397812b..ef54c0a757 100644 --- a/pvlib/ivtools/params.py +++ b/pvlib/ivtools/params.py @@ -35,8 +35,10 @@ def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15), Returns ------- dict - Calculated IV parameters. Keys are 'voc', 'isc', 'vpm', 'imp', 'pmp', - and 'ff'. + 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 ---------- @@ -122,5 +124,6 @@ def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15), result['imp'] = imp result['pmp'] = pmp result['ff'] = ff + result['mp_fit'] = mp_fit return result diff --git a/pvlib/tests/ivtools/test_params.py b/pvlib/tests/ivtools/test_params.py index a6d073a260..c20fbfe0f9 100644 --- a/pvlib/tests/ivtools/test_params.py +++ b/pvlib/tests/ivtools/test_params.py @@ -33,6 +33,10 @@ def test_astm_e1036(v_array, i_array): '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) @@ -52,6 +56,7 @@ def test_astm_e1036_est_isc_voc(v_array, i_array): 'imp': 7.626088301548568, 'pmp': 3.7694489853302127, 'ff': 0.751024747526615} + result.pop('mp_fit') assert result == pytest.approx(expected) @@ -66,6 +71,7 @@ def test_astm_e1036_mpfit_limits(v_array, i_array): 'imp': 7.620032530519718, 'pmp': 3.769189212299219, 'ff': 0.7516875014460312} + result.pop('mp_fit') assert result == pytest.approx(expected) @@ -79,4 +85,5 @@ def test_astm_e1036_fit_points(v_array, i_array): 'imp': 7.626088301548568, 'pmp': 3.7694489853302127, 'ff': 0.7520255886236707} + result.pop('mp_fit') assert result == pytest.approx(expected) From 0955c7c5020b948197cdc4c5b60b213093ebd73b Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Mon, 5 Dec 2022 14:45:39 -0700 Subject: [PATCH 13/24] capitalization --- pvlib/ivtools/params.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/ivtools/params.py b/pvlib/ivtools/params.py index ef54c0a757..63b021184c 100644 --- a/pvlib/ivtools/params.py +++ b/pvlib/ivtools/params.py @@ -26,10 +26,10 @@ def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(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 + 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 + The number of points near short circuit to use for linear fit and Isc calculation Returns From ef969f3f8d3dfcf3c5a5138acb646156b0b81d62 Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Mon, 5 Dec 2022 14:54:49 -0700 Subject: [PATCH 14/24] add polynomial fit order parameter --- pvlib/ivtools/params.py | 8 ++++++-- pvlib/tests/ivtools/test_params.py | 8 ++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pvlib/ivtools/params.py b/pvlib/ivtools/params.py index 63b021184c..238943d24c 100644 --- a/pvlib/ivtools/params.py +++ b/pvlib/ivtools/params.py @@ -8,7 +8,7 @@ def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15), - voc_points=3, isc_points=3): + 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 @@ -31,6 +31,10 @@ def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15), 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 ------- @@ -100,7 +104,7 @@ def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15), filtered = df[mask] # fit polynomial and find max - mp_fit = Poly.fit(filtered['v'], filtered['p'], 4) + 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() diff --git a/pvlib/tests/ivtools/test_params.py b/pvlib/tests/ivtools/test_params.py index c20fbfe0f9..2ad9a3b1a8 100644 --- a/pvlib/tests/ivtools/test_params.py +++ b/pvlib/tests/ivtools/test_params.py @@ -40,6 +40,14 @@ def test_astm_e1036(v_array, i_array): 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 From f7d6faf3a933b973f198efe31dc88fd17dd66b84 Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Mon, 5 Dec 2022 15:24:31 -0700 Subject: [PATCH 15/24] add entry to pv_modeling.rst --- docs/sphinx/source/reference/pv_modeling.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sphinx/source/reference/pv_modeling.rst b/docs/sphinx/source/reference/pv_modeling.rst index 31c380c1bb..fb0300264f 100644 --- a/docs/sphinx/source/reference/pv_modeling.rst +++ b/docs/sphinx/source/reference/pv_modeling.rst @@ -178,6 +178,7 @@ Utilities for working with IV curve data :toctree: generated/ ivtools.utils.rectify_iv_curve + ivtools.params.astm_e1036 Other ----- From 967d394a9f187c686473a7e07f5de451713532a0 Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Mon, 5 Dec 2022 15:29:07 -0700 Subject: [PATCH 16/24] Update whats new --- docs/sphinx/source/whatsnew/v0.9.4.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/sphinx/source/whatsnew/v0.9.4.rst b/docs/sphinx/source/whatsnew/v0.9.4.rst index 00bc89207a..1b96cdb74f 100644 --- a/docs/sphinx/source/whatsnew/v0.9.4.rst +++ b/docs/sphinx/source/whatsnew/v0.9.4.rst @@ -15,6 +15,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 @@ -43,3 +45,4 @@ Contributors * Christian Orner (:ghuser:`chrisorner`) * Saurabh Aneja (:ghuser:`spaneja`) * Marcus Boumans (:ghuser:`bowie2211`) +* Michael Deceglie (:ghuser:`mdeceglie`) From 1dface6d7eea6e9ae4b39ae35492587bb7134630 Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Tue, 6 Dec 2022 08:45:09 -0700 Subject: [PATCH 17/24] fix docstring Co-authored-by: Kevin Anderson --- pvlib/ivtools/params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/ivtools/params.py b/pvlib/ivtools/params.py index 238943d24c..95178760b2 100644 --- a/pvlib/ivtools/params.py +++ b/pvlib/ivtools/params.py @@ -31,7 +31,7 @@ def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15), 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 + mp_fit_order : int, default 4 The order of the polynomial fit of power vs. voltage near maximum power From a817355e3fa211376fe3cbbae7c70afd19261ab1 Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Tue, 6 Dec 2022 08:45:25 -0700 Subject: [PATCH 18/24] update reference Co-authored-by: Kevin Anderson --- pvlib/ivtools/params.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/ivtools/params.py b/pvlib/ivtools/params.py index 95178760b2..d783a87067 100644 --- a/pvlib/ivtools/params.py +++ b/pvlib/ivtools/params.py @@ -47,8 +47,8 @@ def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15), 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 + 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 From f88e6655dc744b43c8275906610a4b0c05830d5f Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Tue, 6 Dec 2022 08:46:39 -0700 Subject: [PATCH 19/24] update whatsnew Co-authored-by: Kevin Anderson --- docs/sphinx/source/whatsnew/v0.9.4.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.9.4.rst b/docs/sphinx/source/whatsnew/v0.9.4.rst index 45e2d3a6d5..dc93b0e932 100644 --- a/docs/sphinx/source/whatsnew/v0.9.4.rst +++ b/docs/sphinx/source/whatsnew/v0.9.4.rst @@ -18,7 +18,7 @@ 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 +* Add a function :py:func:`pvlib.ivtools.params.astm_e1036` to perform ASTM E1036 extraction of IV curve parameters (:pull:`1585`) From 2393b8474b38663d277ed6238c5e0a34cd77204d Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Tue, 6 Dec 2022 08:47:14 -0700 Subject: [PATCH 20/24] update module description Co-authored-by: Kevin Anderson --- pvlib/ivtools/params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/ivtools/params.py b/pvlib/ivtools/params.py index d783a87067..927606a8b1 100644 --- a/pvlib/ivtools/params.py +++ b/pvlib/ivtools/params.py @@ -1,5 +1,5 @@ """ -The ``params`` module contains classes and functions to extract parameters +The ``params`` module contains functions to extract parameters (e.g. Isc and Voc) from current-voltage curves. """ import pandas as pd From 7294dfdd16f555a4a165d4c4fd5ab469250ed2f8 Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Tue, 6 Dec 2022 08:49:32 -0700 Subject: [PATCH 21/24] adjust first quadrant explanation --- pvlib/ivtools/params.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/ivtools/params.py b/pvlib/ivtools/params.py index 927606a8b1..06415aa556 100644 --- a/pvlib/ivtools/params.py +++ b/pvlib/ivtools/params.py @@ -10,8 +10,8 @@ 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 + Extract photovoltaic IV parameters according to ASTM E1036. Assumes that + the power producing portion of the curve is in the first quadrant. Parameters ---------- From f3a0e1884f46ba8f075f54f3450dc1d3b77524bf Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Tue, 6 Dec 2022 10:15:11 -0700 Subject: [PATCH 22/24] Move to utils modules --- docs/sphinx/source/reference/pv_modeling.rst | 2 +- docs/sphinx/source/whatsnew/v0.9.4.rst | 2 +- pvlib/ivtools/__init__.py | 2 +- pvlib/ivtools/params.py | 133 ------------------- pvlib/ivtools/utils.py | 127 ++++++++++++++++++ pvlib/tests/ivtools/test_params.py | 97 -------------- pvlib/tests/ivtools/test_utils.py | 97 +++++++++++++- 7 files changed, 226 insertions(+), 234 deletions(-) delete mode 100644 pvlib/ivtools/params.py delete mode 100644 pvlib/tests/ivtools/test_params.py diff --git a/docs/sphinx/source/reference/pv_modeling.rst b/docs/sphinx/source/reference/pv_modeling.rst index ac834ee67f..c64d08534c 100644 --- a/docs/sphinx/source/reference/pv_modeling.rst +++ b/docs/sphinx/source/reference/pv_modeling.rst @@ -180,7 +180,7 @@ Utilities for working with IV curve data :toctree: generated/ ivtools.utils.rectify_iv_curve - ivtools.params.astm_e1036 + ivtools.utils.astm_e1036 Other ----- diff --git a/docs/sphinx/source/whatsnew/v0.9.4.rst b/docs/sphinx/source/whatsnew/v0.9.4.rst index dc93b0e932..a13f9251bf 100644 --- a/docs/sphinx/source/whatsnew/v0.9.4.rst +++ b/docs/sphinx/source/whatsnew/v0.9.4.rst @@ -18,7 +18,7 @@ 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 function :py:func:`pvlib.ivtools.params.astm_e1036` to perform ASTM E1036 extraction of IV +* Add a function :py:func:`pvlib.ivtools.utils.astm_e1036` to perform ASTM E1036 extraction of IV curve parameters (:pull:`1585`) diff --git a/pvlib/ivtools/__init__.py b/pvlib/ivtools/__init__.py index 565380dd22..de897e2989 100644 --- a/pvlib/ivtools/__init__.py +++ b/pvlib/ivtools/__init__.py @@ -4,4 +4,4 @@ """ -from pvlib.ivtools import sde, sdm, utils, params # noqa: F401 +from pvlib.ivtools import sde, sdm, utils # noqa: F401 diff --git a/pvlib/ivtools/params.py b/pvlib/ivtools/params.py deleted file mode 100644 index 06415aa556..0000000000 --- a/pvlib/ivtools/params.py +++ /dev/null @@ -1,133 +0,0 @@ -""" -The ``params`` module contains 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 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'] - - # 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 diff --git a/pvlib/ivtools/utils.py b/pvlib/ivtools/utils.py index 17eefa31a0..efaf3b0a3d 100644 --- a/pvlib/ivtools/utils.py +++ b/pvlib/ivtools/utils.py @@ -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 @@ -423,3 +424,129 @@ 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'] + + # 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 diff --git a/pvlib/tests/ivtools/test_params.py b/pvlib/tests/ivtools/test_params.py deleted file mode 100644 index 2ad9a3b1a8..0000000000 --- a/pvlib/tests/ivtools/test_params.py +++ /dev/null @@ -1,97 +0,0 @@ -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) diff --git a/pvlib/tests/ivtools/test_utils.py b/pvlib/tests/ivtools/test_utils.py index 8f7826bdc2..d8a35e554d 100644 --- a/pvlib/tests/ivtools/test_utils.py +++ b/pvlib/tests/ivtools/test_utils.py @@ -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 @@ -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) From 25ababf33bf70a757e5316124d5e6e188b637c52 Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Thu, 8 Dec 2022 10:10:29 -0700 Subject: [PATCH 23/24] Simplify logic --- pvlib/ivtools/utils.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/pvlib/ivtools/utils.py b/pvlib/ivtools/utils.py index efaf3b0a3d..dcc591c3b0 100644 --- a/pvlib/ivtools/utils.py +++ b/pvlib/ivtools/utils.py @@ -480,10 +480,6 @@ def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15), 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() @@ -491,19 +487,18 @@ def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15), 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 - 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): + 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 np.isnan(isc): + 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) From 3f306cc17d48f71c71441c536584d89c3c51700f Mon Sep 17 00:00:00 2001 From: Michael Deceglie Date: Wed, 14 Dec 2022 10:49:10 -0700 Subject: [PATCH 24/24] move copyright statement to a comment --- pvlib/ivtools/utils.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pvlib/ivtools/utils.py b/pvlib/ivtools/utils.py index dcc591c3b0..554af24ecc 100644 --- a/pvlib/ivtools/utils.py +++ b/pvlib/ivtools/utils.py @@ -468,13 +468,12 @@ def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15), .. [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. ''' + # 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