Skip to content

Commit c00d419

Browse files
Matt Guttenbergmikofski
Matt Guttenberg
authored andcommitted
ENH: porting pvsyst and single diode param est
from PVLIB_MATLAB by Matt Guttenberg Aug-2016, see pvlib#229 and pvlib#227 Added ported functions and tests to pvlib Added ported versions of calc_theta_phi_exact, lambertw, singlediode, update_io_known_n, update_rsh_fixed_pt and associated tests to pvl-python. Also added a __init__ file ot the tests folder to describe the file folder and allow the tests to be called if necessary. PVsyst_parameter_estimation and singlediode are not complete versions Changed the tests to properly call the appropriate functions Finished the singlediode script Added Test script for singlediode Added tests for singlediode functions and updated scripts Updated the lambertw and singlediode scripts to fix some small errors that were found while testing. Added 38 tests for the singlediode script functions althought more tests will be added. Finished singlediode and corresponding tests Finished adding tests for singlediode that tests all of the functionalities of the script, including properly raising errors. Updated the singlediode script to fix any errors found while testing. Added a statement to the lambertw function to make the calculations slightly faster in the event a -inf case. Added a new file for the Schumaker_QSpline script Finished Schumaker_QSpline script Fixed Schumaker_QSpline script based on test Adjusted all index references to be integer values to avoid the warnings that were displayed. Fixed all logic errors. Fixed the sorting mechanism for the final array. Added test file for Schumaker_QSpline script Added a Schumaker_QSpline test Added a Schumaker_QSpline test (more to be added) and fixed the singlediode_tests script to adhere to PEP8 standards Added a couple more tests for Schumaker_QSpline Added the est_single_diode_parameter port and fixed typos Added the est_single_diode_parameter ported script to pvlib. Fixed a typo in the Schumaker_QSpline script that caused an error under a very particular circumstance. Added brackets in the Schumaker_QSpline test script to get rid of syntax errors. Added tests for the est_single_diode_param script Delete __init__.py Changed one test case for lambertw Fixed some ambiguities in the imports Updated the imports in many of the regular and test scripts to fix some ambiguity issues that might have been occurring during testing Started adding code to the PVsyst_parameter_estimation script Added Classes to singlediode and PVsyst_Parameter_estimation Added a class to move on with the code for the PVsyst_paramter_estimation script and added a class to the singlediode script to wrap up the answers in a more succinct format. Adjusted the singlediode tests to reflect this change. Ported over some of the code for PVsyst_parameter_estimation Ported over some of the sections from PVsyst_parameter_estimation. Added numdiff function to the PVsyst_parameter_estimation script. Since the script uses matplotlib, added the requirement to the setup. Finished PVsyst_parameter_estimation and adjusted outputs of functions Finished porting PVsyst_parameter_estimation except for the section with the robust_fit. Changed the outputs of this function and singlediode from a class to a ordereddict and changed the inputs for PVsyst_parameter_estimation from classes to ordereddicts as well. Changed the setup to include scipy as a required toolbox Added robust fit algorithm to PVsyst_parameter_estimation Added the robust fit algorithm where it was needed to determine desired information. Since this alrogithm used the statsmodels toolbox, the setup was changed to reflect this addition. Removed lambertw since scipy already had a working copy. Script Changes Removed lambertw and associated test script since scipy.special already had a working lambertw that has been tested and verified. Changed associated scripts to reflect this change. Some bug fixes for Pvsyst_paramter_estimation Removed some coded scripts and updated PVsyst_parameter_estimation Removed i_from_v, v_from_i and singlediode and used the already coded and tested versions within pvlib. Updated PVsyst_parameter_estimation based on some bug fixes. More bug fixes Fixed additional typos and other errors that have prevented the code from completing. Now that the code runs to completion, additional checks will be run to determine whether the converged values are appropriate. Updated calc_theta_phi_exact to handle an edge case modified calc_theta_phi_exact to handle the edge case where nnsvth was equal to 0. Brought back lambertw script for use in update_rsh_fixed_pt Reintroduced lambertw script for use in update_rsh_fixed_pt since the lambertw script was having problems for cases where rsh was extremely high in calc_theta_phi_exact A couple mroe PVsyst_parameter_estimation bug fixes Fixed import errors and removed unused lines Fixed an import error in update_io_known_n and removed v_from_i from the tests since the code is using the code that already exists and has been tested in pvsystem. Removed an unused import in update_rsh_fixed_pt Fixed typos / errors Fixed an error in PVsyst_parameter_estimation in the numdiff function where the summations were not occuring properly. Fixed a typo in pvsystem where a parameter was accidentally changed. Final bug fixes for PVsyst_parameter_estimation Fixed a typo in one of the plots Style Update Modified all written scripts to adhere to the flake8 and pep8-naming guidelines. Updated tests as suggested by Will Documentation Update Updated script documentation for each of the written scripts to fit better with the pvlib documentation Added Documents from Cliff
1 parent c9adf8b commit c00d419

18 files changed

+16574
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,4 @@ coverage.xml
8686
#Ignore some notebooks
8787
*.ipynb
8888
!docs/tutorials/*.ipynb
89+
*.idea

pvlib/PVsyst_parameter_estimation.py

Lines changed: 958 additions & 0 deletions
Large diffs are not rendered by default.

pvlib/Schumaker_QSpline.py

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import numpy as np
2+
3+
4+
def schumaker_qspline(x, y):
5+
"""
6+
Schumaker_QSpline fits a quadratic spline which preserves monotonicity and
7+
convexity in the data.
8+
9+
Syntax
10+
------
11+
outa, outxk, outy, kflag = schumaker_qspline(x, y)
12+
13+
Description
14+
-----------
15+
Calculates coefficients for C1 quadratic spline interpolating data X, Y
16+
where length(x) = N and length(y) = N, which preserves monotonicity and
17+
convexity in the data.
18+
19+
Parameters
20+
----------
21+
x, y: numpy arrays of length N containing (x, y) points between which the
22+
spline will interpolate.
23+
24+
Returns
25+
-------
26+
outa: a Nx3 matrix of coefficients where the ith row defines the quadratic
27+
interpolant between xk_i to xk_(i+1), i.e., y = A[i, 0] *
28+
(x - xk[i]] ** 2 + A[i, 1] * (x - xk[i]) + A[i, 2]
29+
outxk: an ordered vector of knots, i.e., values xk_i where the spline
30+
changes coefficients. All values in x are used as knots. However
31+
the algorithm may insert additional knots between data points in x
32+
where changes in convexity are indicated by the (numerical)
33+
derivative. Consequently output outxk has length >= length(x).
34+
outy: y values corresponding to the knots in outxk. Contains the original
35+
data points, y, and also y-values estimated from the spline at the
36+
inserted knots.
37+
kflag: a vector of length(outxk) of logicals, which are set to true for
38+
elements of outxk that are knots inserted by the algorithm.
39+
40+
References
41+
----------
42+
[1] PVLib MATLAB
43+
[2] L. L. Schumaker, "On Shape Preserving Quadratic Spline Interpolation",
44+
SIAM Journal on Numerical Analysis 20(4), August 1983, pp 854 - 864
45+
[3] M. H. Lam, "Monotone and Convex Quadratic Spline Interpolation",
46+
Virginia Journal of Science 41(1), Spring 1990
47+
"""
48+
49+
# A small number used to decide when a slope is equivalent to zero
50+
eps = 1e-6
51+
52+
# Make sure vectors are 1D arrays
53+
if x.ndim != 1.:
54+
x = x.flatten([range(x.size)])
55+
if y.ndim != 1.:
56+
y = y.flatten([range(y.size)])
57+
58+
n = len(x)
59+
60+
# compute various values used by the algorithm: differences, length of line
61+
# segments between data points, and ratios of differences.
62+
delx = np.diff(x) # delx[i] = x[i + 1] - x[i]
63+
dely = np.diff(y)
64+
65+
delta = dely / delx
66+
67+
# Calculate first derivative at each x value per [3]
68+
69+
s = np.zeros(x.shape)
70+
71+
left = np.append(0., delta)
72+
right = np.append(delta, 0.)
73+
74+
pdelta = left * right
75+
76+
u = pdelta > 0.
77+
78+
# [3], Eq. 9 for interior points
79+
# fix tuning parameters in [2], Eq 9 at chi = .5 and eta = .5
80+
s[u] = pdelta[u] / (.5 * left[u] + .5 * right[u])
81+
82+
# [3], Eq. 7 for left endpoint
83+
if delta[0] * (2. * delta[0] - s[1]) > 0.:
84+
s[0] = 2. * delta[0] - s[1]
85+
86+
# [3], Eq. 8 for right endpoint
87+
if delta[n - 2] * (2. * delta[n - 2] - s[n - 2]) > 0.:
88+
s[n - 1] = 2. * delta[n - 2] - s[n - 2]
89+
90+
# determine knots. Start with initial pointsx
91+
# [2], Algorithm 4.1 first 'if' condition of step 5 defines intervals
92+
# which won't get internal knots
93+
tests = s[0.:(n - 1)] + s[1:n]
94+
u = np.abs(tests - 2. * delta[0:(n - 1)]) <= eps
95+
# u = true for an interval which will not get an internal knot
96+
97+
k = n + sum(~u) # total number of knots = original data + inserted knots
98+
99+
# set up output arrays
100+
# knot locations, first n - 1 and very last (n + k) are original data
101+
xk = np.zeros(k)
102+
yk = np.zeros(k) # function values at knot locations
103+
# logicals that will indicate where additional knots are inserted
104+
flag = np.zeros(k, dtype=bool)
105+
a = np.zeros((k, 3.))
106+
107+
# structures needed to compute coefficients, have to be maintained in
108+
# association with each knot
109+
110+
tmpx = x[0:(n - 1)]
111+
tmpy = y[0:(n - 1)]
112+
tmpx2 = x[1:n]
113+
tmps = s[0.:(n - 1)]
114+
tmps2 = s[1:n]
115+
diffs = np.diff(s)
116+
117+
# structure to contain information associated with each knot, used to
118+
# calculate coefficients
119+
uu = np.zeros((k, 6.))
120+
121+
uu[0:(n - 1), :] = np.array([tmpx, tmpx2, tmpy, tmps, tmps2, delta]).T
122+
123+
# [2], Algorithm 4.1 subpart 1 of Step 5
124+
# original x values that are left points of intervals without internal
125+
# knots
126+
xk[u] = tmpx[u]
127+
yk[u] = tmpy[u]
128+
# constant term for each polynomial for intervals without knots
129+
a[u, 2] = tmpy[u]
130+
a[u, 1] = s[u]
131+
a[u, 0] = .5 * diffs[u] / delx[u] # leading coefficients
132+
133+
# [2], Algorithm 4.1 subpart 2 of Step 5
134+
# original x values that are left points of intervals with internal knots
135+
xk[~u] = tmpx[~u]
136+
yk[~u] = tmpy[~u]
137+
138+
aa = s[0:(n - 1)] - delta[0:(n - 1)]
139+
b = s[1:n] - delta[0:(n - 1)]
140+
141+
sbar = np.zeros(k)
142+
eta = np.zeros(k)
143+
# will contain mapping from the left points of intervals containing an
144+
# added knot to each inverval's internal knot value
145+
xi = np.zeros(k)
146+
147+
t0 = aa * b >= 0
148+
# first 'else' in Algorithm 4.1 Step 5
149+
v = np.logical_and(~u, t0[0:len(u)])
150+
q = np.sum(v) # number of this type of knot to add
151+
152+
if q > 0.:
153+
xk[(n - 1):(n + q - 1)] = .5 * (tmpx[v] + tmpx2[v]) # knot location
154+
uu[(n - 1):(n + q - 1), :] = np.array([tmpx[v], tmpx2[v], tmpy[v],
155+
tmps[v], tmps2[v], delta[v]]).T
156+
xi[v] = xk[(n - 1):(n + q - 1)]
157+
158+
t1 = np.abs(aa) > np.abs(b)
159+
w = np.logical_and(~u, ~v) # second 'else' in Algorithm 4.1 Step 5
160+
w = np.logical_and(w, t1)
161+
r = np.sum(w)
162+
163+
if r > 0.:
164+
xk[(n + q - 1):(n + q + r - 1)] = tmpx2[w] + aa[w] * delx[w] / diffs[w]
165+
uu[(n + q - 1):(n + q + r - 1), :] = np.array([tmpx[w], tmpx2[w],
166+
tmpy[w], tmps[w],
167+
tmps2[w], delta[w]]).T
168+
xi[w] = xk[(n + q - 1):(n + q + r - 1)]
169+
170+
z = np.logical_and(~u, ~v) # last 'else' in Algorithm 4.1 Step 5
171+
z = np.logical_and(z, ~w)
172+
ss = np.sum(z)
173+
174+
if ss > 0.:
175+
xk[(n + q + r - 1):(n + q + r + ss - 1)] = tmpx[z] + b[z] * delx[z] / \
176+
diffs[z]
177+
uu[(n + q + r - 1):(n + q + r + ss - 1), :] = \
178+
np.array([tmpx[z], tmpx2[z], tmpy[z], tmps[z], tmps2[z],
179+
delta[z]]).T
180+
xi[z] = xk[(n + q + r - 1):(n + q + r + ss - 1)]
181+
182+
# define polynomial coefficients for intervals with added knots
183+
ff = ~u
184+
sbar[ff] = (2 * uu[ff, 5] - uu[ff, 4]) + \
185+
(uu[ff, 4] - uu[ff, 3]) * (xi[ff] - uu[ff, 0]) / (uu[ff, 1] -
186+
uu[ff, 0])
187+
eta[ff] = (sbar[ff] - uu[ff, 3]) / (xi[ff] - uu[ff, 0])
188+
189+
sbar[(n - 1):(n + q + r + ss - 1)] = \
190+
(2 * uu[(n - 1):(n + q + r + ss - 1), 5] -
191+
uu[(n - 1):(n + q + r + ss - 1), 4]) + \
192+
(uu[(n - 1):(n + q + r + ss - 1), 4] -
193+
uu[(n - 1):(n + q + r + ss - 1), 3]) * \
194+
(xk[(n - 1):(n + q + r + ss - 1)] -
195+
uu[(n - 1):(n + q + r + ss - 1), 0]) / \
196+
(uu[(n - 1):(n + q + r + ss - 1), 1] -
197+
uu[(n - 1):(n + q + r + ss - 1), 0])
198+
eta[(n - 1):(n + q + r + ss - 1)] = \
199+
(sbar[(n - 1):(n + q + r + ss - 1)] -
200+
uu[(n - 1):(n + q + r + ss - 1), 3]) / \
201+
(xk[(n - 1):(n + q + r + ss - 1)] -
202+
uu[(n - 1):(n + q + r + ss - 1), 0])
203+
204+
# constant term for polynomial for intervals with internal knots
205+
a[~u, 2] = uu[~u, 2]
206+
a[~u, 1] = uu[~u, 3]
207+
a[~u, 0] = .5 * eta[~u] # leading coefficient
208+
209+
a[(n - 1):(n + q + r + ss - 1), 2] = \
210+
uu[(n - 1):(n + q + r + ss - 1), 2] + \
211+
uu[(n - 1):(n + q + r + ss - 1), 3] * \
212+
(xk[(n - 1):(n + q + r + ss - 1)] -
213+
uu[(n - 1):(n + q + r + ss - 1), 0]) + \
214+
.5 * eta[(n - 1):(n + q + r + ss - 1)] * \
215+
(xk[(n - 1):(n + q + r + ss - 1)] -
216+
uu[(n - 1):(n + q + r + ss - 1), 0]) ** 2.
217+
a[(n - 1):(n + q + r + ss - 1), 1] = sbar[(n - 1):(n + q + r + ss - 1)]
218+
a[(n - 1):(n + q + r + ss - 1), 0] = \
219+
.5 * (uu[(n - 1):(n + q + r + ss - 1), 4] -
220+
sbar[(n - 1):(n + q + r + ss - 1)]) / \
221+
(uu[(n - 1):(n + q + r + ss - 1), 1] -
222+
uu[(n - 1):(n + q + r + ss - 1), 0])
223+
224+
yk[(n - 1):(n + q + r + ss - 1)] = a[(n - 1):(n + q + r + ss - 1), 2]
225+
226+
xk[n + q + r + ss - 1] = x[n - 1]
227+
yk[n + q + r + ss - 1] = y[n - 1]
228+
flag[(n - 1):(n + q + r + ss - 1)] = True # these are all inserted knots
229+
230+
tmp = np.vstack((xk, a.T, yk, flag)).T
231+
# sort output in terms of increasing x (original plus added knots)
232+
tmp2 = tmp[tmp[:, 0].argsort(kind='mergesort')]
233+
outxk = tmp2[:, 0]
234+
outn = len(outxk)
235+
outa = tmp2[0:(outn - 1), 1:4]
236+
outy = tmp2[:, 4]
237+
kflag = tmp2[:, 5]
238+
return outa, outxk, outy, kflag

pvlib/calc_theta_phi_exact.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import numpy as np
2+
from pvlib.lambertw import lambertw
3+
4+
5+
def calc_theta_phi_exact(imp, il, vmp, io, nnsvth, rs, rsh):
6+
"""
7+
CALC_THETA_PHI_EXACT computes Lambert W values appearing in the analytic
8+
solutions to the single diode equation for the max power point.
9+
10+
Syntax
11+
------
12+
theta, phi = calc_theta_phi_exact(imp, il, vmp, io, nnsvth, rs, rsh)
13+
14+
Description
15+
-----------
16+
calc_theta_phi_exact calculates values for the Lambert W function which
17+
are used in the analytic solutions for the single diode equation at the
18+
maximum power point. For V=V(I),
19+
phi = W(Io*Rsh/n*Vth * exp((IL + Io - Imp)*Rsh/n*Vth)). For I=I(V),
20+
theta = W(Rs*Io/n*Vth *
21+
Rsh/ (Rsh+Rs) * exp(Rsh/ (Rsh+Rs)*((Rs(IL+Io) + V)/n*Vth))
22+
23+
Parameters
24+
----------
25+
imp: a numpy array of length N of values for Imp (A)
26+
il: a numpy array of length N of values for the light current IL (A)
27+
vmp: a numpy array of length N of values for Vmp (V)
28+
io: a numpy array of length N of values for Io (A)
29+
nnsvth: a numpy array of length N of values for the diode factor x
30+
thermal voltage for the module, equal to Ns
31+
(number of cells in series) x Vth
32+
(thermal voltage per cell).
33+
rs: a numpy array of length N of values for the series resistance (ohm)
34+
rsh: a numpy array of length N of values for the shunt resistance (ohm)
35+
36+
Returns
37+
-------
38+
theta: a numpy array of values for the Lamber W function for solving
39+
I = I(V)
40+
phi: a numpy array of values for the Lambert W function for solving
41+
V = V(I)
42+
43+
References
44+
----------
45+
[1] PVLib MATLAB
46+
[2] C. Hansen, Parameter Estimation for Single Diode Models of Photovoltaic
47+
Modules, Sandia National Laboratories Report SAND2015-XXXX
48+
[3] A. Jain, A. Kapoor, "Exact analytical solutions of the parameters of
49+
real solar cells using Lambert W-function", Solar Energy Materials and
50+
Solar Cells, 81 (2004) 269-277.
51+
"""
52+
53+
# Argument for Lambert W function involved in V = V(I) [2] Eq. 12; [3]
54+
# Eq. 3
55+
if any(nnsvth == 0.):
56+
argw = []
57+
for i in nnsvth:
58+
if i == 0.:
59+
argw.append(float("Inf"))
60+
else:
61+
argw.append(rsh * io / i * np.exp(rsh * (il + io - imp) / i))
62+
argw = np.array(argw)
63+
else:
64+
argw = rsh * io / nnsvth * np.exp(rsh * (il + io - imp) / nnsvth)
65+
u = argw > 0
66+
w = np.zeros(len(u))
67+
w[~u] = float("Nan")
68+
if any(argw[u] == float("Inf")):
69+
tmp = []
70+
for i in argw[u]:
71+
if i == float("Inf"):
72+
tmp.append(float("Nan"))
73+
else:
74+
tmp.append(lambertw(i).real)
75+
tmp = np.array(tmp)
76+
else:
77+
tmp = lambertw(argw[u]).real
78+
ff = np.isnan(tmp)
79+
80+
# NaN where argw overflows. Switch to log space to evaluate
81+
if any(ff):
82+
logargw = np.log(rsh[u]) + np.log(io[u]) - np.log(nnsvth[u]) + \
83+
rsh[u] * (il[u] + io[u] - imp[u]) / nnsvth[u]
84+
# Three iterations of Newton-Raphson method to solve w+log(w)=logargW.
85+
# The initial guess is w=logargW. Where direct evaluation (above)
86+
# results in NaN from overflow, 3 iterations of Newton's method gives
87+
# approximately 8 digits of precision.
88+
x = logargw
89+
for i in range(3):
90+
x *= ((1. - np.log(x) + logargw) / (1. + x))
91+
tmp[ff] = x[ff]
92+
w[u] = tmp
93+
phi = np.transpose(w)
94+
95+
# Argument for Lambert W function involved in I = I(V) [2] Eq. 11; [3]
96+
# E1. 2
97+
if any(nnsvth == 0.):
98+
argw = []
99+
for i in nnsvth:
100+
if i == 0.:
101+
argw.append(float("Inf"))
102+
else:
103+
argw.append(rsh / (rsh + rs) * rs * io / i *
104+
np.exp(rsh / (rsh + rs) * (rs * (il + io) + vmp) /
105+
i))
106+
argw = np.array(argw)
107+
else:
108+
argw = rsh / (rsh + rs) * rs * io / nnsvth * \
109+
np.exp(rsh / (rsh + rs) * (rs * (il + io) + vmp) / nnsvth)
110+
u = argw > 0
111+
w = np.zeros(len(u))
112+
w[~u] = float("Nan")
113+
if any(argw[u] == float("Inf")):
114+
tmp = []
115+
for i in argw[u]:
116+
if i == float("Inf"):
117+
tmp.append(float("Nan"))
118+
else:
119+
tmp.append(lambertw(i).real)
120+
tmp = np.array(tmp)
121+
else:
122+
tmp = lambertw(argw[u]).real
123+
ff = np.isnan(tmp)
124+
125+
# NaN where argw overflows. Switch to log space to evaluate
126+
if any(ff):
127+
logargw = np.log(rsh[u]) / (rsh[u] + rs[u]) + np.log(rs[u]) + \
128+
np.log(io[u]) - np.log(nnsvth[u]) + \
129+
(rsh[u] / (rsh[u] + rs[u])) * \
130+
(rs[u] * (il[u] + io[u]) + vmp[u]) / nnsvth[u]
131+
# Three iterations of Newton-Raphson method to solve w+log(w)=logargW.
132+
# The initial guess is w=logargW. Where direct evaluation (above)
133+
# results in NaN from overflow, 3 iterations of Newton's method gives
134+
# approximately 8 digits of precision.
135+
x = logargw
136+
for i in range(3):
137+
x *= ((1. - np.log(x) + logargw) / (1. + x))
138+
tmp[ff] = x[ff]
139+
w[u] = tmp
140+
theta = np.transpose(w)
141+
return theta, phi

0 commit comments

Comments
 (0)