-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathsde.py
More file actions
471 lines (375 loc) · 16.6 KB
/
sde.py
File metadata and controls
471 lines (375 loc) · 16.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
"""
The ``sde`` module contains functions to fit the single diode equation.
Function names should follow the pattern "fit_" + fitting method.
"""
import numpy as np
from pvlib.ivtools.utils import _schumaker_qspline
def fit_sandia_simple(voltage, current, v_oc=None, i_sc=None, v_mp_i_mp=None,
vlim=0.2, ilim=0.1):
r"""
Fits the single diode equation (SDE) to an IV curve.
Parameters
----------
voltage : ndarray
1D array of `float` type containing voltage at each point on the IV
curve, increasing from 0 to ``v_oc`` inclusive. [V]
current : ndarray
1D array of `float` type containing current at each point on the IV
curve, from ``i_sc`` to 0 inclusive. [A]
v_oc : float, optional
Open circuit voltage. If not provided, ``v_oc`` is taken as the
last point in the ``voltage`` array. [V]
i_sc : float, optional
Short circuit current. If not provided, ``i_sc`` is taken as the
first point in the ``current`` array. [A]
v_mp_i_mp : tuple of float, optional
Voltage, current at maximum power point. If not provided, the maximum
power point is found at the maximum of ``voltage`` \times ``current``.
[V], [A]
vlim : float, default 0.2
Defines portion of IV curve where the exponential term in the single
diode equation can be neglected, i.e.
``voltage`` <= ``vlim`` x ``v_oc``. [V]
ilim : float, default 0.1
Defines portion of the IV curve where the exponential term in the
single diode equation is significant, approximately defined by
``current`` < (1 - ``ilim``) x ``i_sc``. [A]
Returns
-------
photocurrent : float
photocurrent [A]
saturation_current : float
dark (saturation) current [A]
resistance_series : float
series resistance [ohm]
resistance_shunt : float
shunt (parallel) resistance [ohm]
nNsVth : float
product of thermal voltage ``Vth`` [V], diode ideality factor
``n``, and number of series cells ``Ns``. [V]
Raises
------
RuntimeError
if parameter extraction is not successful.
Notes
-----
Inputs ``voltage``, ``current``, ``v_oc``, ``i_sc`` and ``v_mp_i_mp`` are
assumed to be from a single IV curve at constant irradiance and cell
temperature.
:py:func:`fit_sandia_simple` obtains values for the five parameters for
the single diode equation [1]_:
.. math::
I = I_{L} - I_{0} (\exp \frac{V + I R_{s}}{nNsVth} - 1)
- \frac{V + I R_{s}}{R_{sh}}
See :py:func:`pvlib.pvsystem.singlediode` for definition of the parameters.
The extraction method [2]_ proceeds in six steps.
1. In the single diode equation, replace :math:`R_{sh} = 1/G_{p}` and
re-arrange
.. math::
I = \frac{I_{L}}{1 + G_{p} R_{s}} - \frac{G_{p} V}{1 + G_{p} R_{s}}
- \frac{I_{0}}{1 + G_{p} R_{s}} (\exp(\frac{V + I R_{s}}{nN_sV_{th}})
- 1)
2. The linear portion of the IV curve is defined as
:math:`V \le vlim \times v_{oc}`. Over this portion of the IV curve,
.. math::
\frac{I_{0}}{1 + G_{p} R_{s}} (\exp(\frac{V + I R_{s}}{nN_sV_{th}})
- 1) \approx 0
3. Fit the linear portion of the IV curve with a line.
.. math::
I &\approx \frac{I_{L}}{1 + G_{p} R_{s}}
- \frac{G_{p}}{1 + G_{p}R_{s}} V \\
&= \beta_{0} + \beta_{1} V
4. The exponential portion of the IV curve is defined by
:math:`\beta_{0} + \beta_{1} \times V - I > ilim \times i_{sc}`.
Over this portion of the curve,
:math:`\exp((V + IR_s)/{nN_sV_{th}}) \gg 1` so that
.. math::
\exp(\frac{V + I R_{s}}{nN_sV_{th}}) - 1 \approx
\exp(\frac{V + I R_{s}}{nN_sV_{th}})
5. Fit the exponential portion of the IV curve.
.. math::
\log(\beta_{0} - \beta_{1} V - I)
&\approx \log(\frac{I_{0}}{1 + G_{p} R_{s}}) + \frac{V}{nN_sV_{th}}
+ \frac{I R_{s}}{nN_sV_{th}} \\
&= \beta_{2} + \beta_{3} V + \beta_{4} I
6. Calculate values for ``IL, I0, Rs, Rsh,`` and ``nNsVth`` from the
regression coefficents :math:`\beta_{0}, \beta_{1}, \beta_{3}` and
:math:`\beta_{4}`.
References
----------
.. [1] S.R. Wenham, M.A. Green, M.E. Watt, "Applied Photovoltaics" ISBN
0 86758 909 4
.. [2] C. B. Jones, C. W. Hansen, "Single Diode Parameter Extraction from
In-Field Photovoltaic I-V Curves on a Single Board Computer", 46th IEEE
Photovoltaic Specialist Conference, Chicago, IL, 2019.
:doi:`10.1109/PVSC40753.2019.8981330`
"""
# If not provided, extract v_oc, i_sc, v_mp and i_mp from the IV curve data
if v_oc is None:
v_oc = voltage[-1]
if i_sc is None:
i_sc = current[0]
if v_mp_i_mp is not None:
v_mp, i_mp = v_mp_i_mp
else:
v_mp, i_mp = _find_mp(voltage, current)
# Find beta0 and beta1 from linear portion of the IV curve
beta0, beta1 = _sandia_beta0_beta1(voltage, current, vlim, v_oc)
# Find beta3 and beta4 from the exponential portion of the IV curve
beta3, beta4 = _sandia_beta3_beta4(voltage, current, beta0, beta1, ilim,
i_sc)
# calculate single diode parameters from regression coefficients
return _sandia_simple_params(beta0, beta1, beta3, beta4, v_mp, i_mp, v_oc)
def _find_mp(voltage, current):
"""
Finds voltage and current at maximum power point.
Parameters
----------
voltage : ndarray
1D array containing voltage at each point on the IV curve, increasing
from 0 to v_oc inclusive, of `float` type. [V]
current : ndarray
1D array containing current at each point on the IV curve, decreasing
from i_sc to 0 inclusive, of `float` type. [A]
Returns
-------
v_mp, i_mp : tuple
voltage ``v_mp`` and current ``i_mp`` at the maximum power point. [V],
[A]
"""
p = voltage * current
idx = np.argmax(p)
return voltage[idx], current[idx]
def _sandia_beta0_beta1(v, i, vlim, v_oc):
# Used by fit_sandia_simple.
# Get intercept and slope of linear portion of IV curve.
# Start with V =< vlim * v_oc, extend by adding points until slope is
# negative (downward).
beta0 = np.nan
beta1 = np.nan
first_idx = np.searchsorted(v, vlim * v_oc)
for idx in range(first_idx, len(v)):
coef = np.polyfit(v[:idx], i[:idx], deg=1)
if coef[0] < 0:
# intercept term
beta0 = coef[1].item()
# sign change of slope to get positive parameter value
beta1 = -coef[0].item()
break
if any(np.isnan([beta0, beta1])):
raise RuntimeError("Parameter extraction failed: beta0={}, beta1={}"
.format(beta0, beta1))
else:
return beta0, beta1
def _sandia_beta3_beta4(voltage, current, beta0, beta1, ilim, i_sc):
# Used by fit_sde_sandia.
# Subtract the IV curve from the linear fit.
y = beta0 - beta1 * voltage - current
x = np.array([np.ones_like(voltage), voltage, current]).T
# Select points where y > ilim * i_sc to regress log(y) onto x
idx = (y > ilim * i_sc)
result = np.linalg.lstsq(x[idx], np.log(y[idx]), rcond=None)
coef = result[0]
beta3 = coef[1].item()
beta4 = coef[2].item()
if any(np.isnan([beta3, beta4])):
raise RuntimeError("Parameter extraction failed: beta3={}, beta4={}"
.format(beta3, beta4))
else:
return beta3, beta4
def _sandia_simple_params(beta0, beta1, beta3, beta4, v_mp, i_mp, v_oc):
# Used by fit_sandia_simple.
nNsVth = 1.0 / beta3
rs = beta4 / beta3
gsh = beta1 / (1.0 - rs * beta1)
rsh = 1.0 / gsh
iph = (1 + gsh * rs) * beta0
# calculate I0
io_vmp = _calc_I0(v_mp, i_mp, iph, gsh, rs, nNsVth)
io_voc = _calc_I0(v_oc, 0, iph, gsh, rs, nNsVth)
if any(np.isnan([io_vmp, io_voc])) or ((io_vmp <= 0) and (io_voc <= 0)):
raise RuntimeError("Parameter extraction failed: I0 is undetermined.")
elif (io_vmp > 0) and (io_voc > 0):
io = 0.5 * (io_vmp + io_voc)
elif (io_vmp > 0):
io = io_vmp
else: # io_voc > 0
io = io_voc
return iph, io, rs, rsh, nNsVth
def _calc_I0(voltage, current, iph, gsh, rs, nNsVth):
return (iph - current - gsh * (voltage + rs * current)) / \
np.expm1((voltage + rs * current) / nNsVth)
def _fit_sandia_cocontent(voltage, current, nsvth):
"""
Regression technique to fit the single diode equation to data for a single
IV curve.
In general, not reliable for estimating parameters other than Rsh.
Parameters
----------
voltage : numeric
voltage for the IV curve in increasing order, the first value must be
0, the last value is taken as ``Voc``. [V]
current : numeric
current for the IV curve corresponding to ``voltage``, the first value
is taken as ``Isc``, the last value must be 0. [A]
nsvth : numeric
the thermal voltage for the module, equal to ``Ns`` (number of cells in
series) times ``Vth`` (thermal voltage per cell). [V]
Returns
-------
iph : numeric
photocurrent [A]
io : numeric
dark current [A]
rs : numeric
shunt resistance [ohm]
rsh : numeric
series resistance [ohm]
n : numeric
diode (ideality) factor [unitless]
Raises
------
ValueError
if ``voltage`` and ``current`` are different lengths.
ValueError
if ``len(voltage)`` < 6
Notes
-----
Ported from PVLib Matlab [1]_. This function uses a regression technique
described in [2]_ to fit the single diode equation to data for a single IV
curve. The method extends ideas in [3]_ where the co-content is regressed
onto predictors involving voltage and current. Here, a principal component
transformation of ``(voltage, current)`` prior to regression to reduce the
effects of colinearity between voltage and current.
Although values for each of the five parameters are returned,
testing has shown that values for ``Rs``, ``Io`` and ``n`` may be negative
or imaginary even for IV curve data without obvious flaws.
References
----------
.. [1] PVLib MATLAB https://github.com/sandialabs/MATLAB_PV_LIB
.. [2] C. Hansen, "Parameter Estimation for Single Diode Models of
Photovoltaic Modules", Sandia National Laboratories Report SAND2015-2065
.. [3] A. Ortiz-Conde, F. Garci'a Sa'nchez, J. Murci, "New method to
extract the model parameters of solar cells from the explicit analytic
solutions of their illuminated I-V characteristics", Solar Energy
Materials and Solar Cells 90, pp 352 - 361, 2006.
"""
if len(current) != len(voltage):
raise ValueError("voltage and current should have the same "
"length")
if len(voltage) < 6:
raise ValueError("at least 6 voltage points are required; ~50 are "
"recommended")
isc = current[0] # short circuit current
voc = voltage[-1] # open circuit voltage
# Fit quadratic spline to IV curve in order to compute the co-content
# (i.e., integral of Isc - I over V) more accurately
[t, c, yhat, kflag] = _schumaker_qspline(voltage, current)
# Calculate co-content integral
cci = _cocontent(t, c, isc, kflag)
# Regress co-content onto voltage and current predictors
beta = _cocontent_regress(voltage, current, voc, isc, cci)
# Extract five parameter values from regression coefficients.
# Equation 11, [3]
betagp = beta[3] * 2.
# Equation 12, [3]
betars = (np.sqrt(1. + 16. * beta[3] * beta[4]) - 1.) / (4. * beta[3])
# Equation 13, [3]
betan = (beta[0] * (np.sqrt(1. + 16. * beta[3] * beta[4]) - 1.) + 4. *
beta[1] * beta[3]) / (4. * beta[3] * nsvth)
# Single diode equation at Voc, approximating Iph + Io by Isc
betaio = (isc - voc * betagp) / (np.exp(voc / (betan * nsvth)))
# Single diode equation at Isc, using Rsh, Rs, n and Io that were
# determined above
betaiph = isc - betaio + betaio * np.exp(isc / (betan * nsvth)) + \
isc * betars * betagp
iph = betaiph
io = betaio
rsh = 1 / betagp
rs = betars
n = betan
return iph, io, rs, rsh, n
def _cocontent(v, c, isc, kflag):
# Used by fit_sandia_cocontent
# calculate co-content integral by numerical integration of
# i = (Isc - I) over v
# Here, i = Isc - I is assumed to be represented by the quadratic spline
# with coefficients in input c, at the discrete sequence of knots in v
xn = len(v)
delx = v[1:] - v[:-1]
tmp = np.array([1. / 3., .5, 1.])
ss = np.tile(tmp, [xn - 1, 1])
cc = c * ss # cast coefficients to a convenient shape
# compute integral on each interval
tmpint = np.sum(cc * np.array([delx ** 3, delx ** 2, delx]).T, 1)
tmpint = np.append(0., tmpint)
# compute co-content = Int_0^V (Isc - I) dV
scc = np.zeros(xn)
# Use trapezoid rule for the first 5 intervals due to spline being
# unreliable near the left endpoint
scc[0:5] = isc * v[0:5] - np.cumsum(tmpint[0:5]) # by spline
scc[5:(xn - 5)] = isc * (v[5:(xn - 5)] - v[4]) - \
np.cumsum(tmpint[5:(xn - 5)]) + scc[4]
# Use trapezoid rule for the last 5 intervals due to spline being
# unreliable near the right endpoint
scc[(xn - 5):xn] = isc * (v[(xn - 5):xn] - v[xn - 6]) - \
np.cumsum(tmpint[(xn - 5):xn]) + scc[xn - 6]
# For estimating diode equation parameters only use original data points,
# not at any knots added by the quadratic spline fit
cci = scc[~kflag.astype(bool)]
return cci
def _cocontent_regress(v, i, voc, isc, cci):
# Used by fit_sandia_content
# For the method coded here see Appendix C of [2] SAND2015-2065
# predictor variables for regression of CC
x = np.vstack((v, isc - i, v * (isc - i), v * v, (i - isc) ** 2)).T
# define principal components transformation to shift, scale and rotate
# V and I before the regression.
tmpx = x[:, 0:2]
tmpx_length = tmpx.shape[0]
tmpx_mean = np.mean(tmpx, axis=0)
tmpx_std = np.std(tmpx, axis=0, ddof=1)
tmpx_zscore = (tmpx - np.tile(tmpx_mean, [tmpx_length, 1])) / \
np.tile(tmpx_std, [tmpx_length, 1])
tmpx_d, tmpx_v = np.linalg.eig(np.cov(tmpx_zscore.T))
idx = np.argsort(tmpx_d)[::-1]
ev1 = tmpx_v[:, idx[0]]
# Second component set to be orthogonal and rotated counterclockwise by 90.
ev2 = np.dot(np.array([[0., -1.], [1., 0.]]), ev1)
r = np.array([ev1, ev2]) # principal components transformation
s = np.dot(tmpx_zscore, r)
# [V, I] shift and scaled by zscore, rotated by r
scc = cci - np.mean(cci, axis=0) # center co-content values
col1 = np.ones(len(scc))
# predictors. Shifting makes a constant term necessary in the regression
# model
sx = np.vstack((s[:, 0], s[:, 1], s[:, 0] * s[:, 1], s[:, 0] * s[:, 0],
s[:, 1] * s[:, 1], col1)).T
gamma = np.linalg.lstsq(sx, scc, rcond=None)[0]
# coefficients from regression in rotated coordinates
# Principle components transformation steps
# Matrix which relates principal components transformation R to the mapping
# between [V' I' V'I' V'^2 I'^2] and sx, where prime ' indicates shifted
# and scaled data. Used to translate from regression coefficients in
# rotated coordinates to coefficients in initial V, I coordinates.
mb = np.array([[r[0, 0], r[1, 0], 0., 0., 0.], [r[0, 1], r[1, 1], 0., 0.,
0.],
[0., 0., r[0, 0] * r[1, 1] + r[0, 1] * r[1, 0], 2. *
r[0, 0] * r[0, 1], 2. * r[1, 0] * r[1, 1]],
[0., 0., r[0, 0] * r[1, 0], r[0, 0] ** 2., r[1, 0] ** 2.],
[0., 0., r[0, 1] * r[1, 1], r[0, 1] ** 2., r[1, 1] ** 2.]])
# matrix which is used to undo effect of shifting and scaling on regression
# coefficients.
ma = np.array([[np.std(v, ddof=1), 0., np.std(v, ddof=1) *
np.mean(isc - i), 2. * np.std(v, ddof=1) * np.mean(v),
0.], [0., np.std(isc - i, ddof=1), np.std(isc - i, ddof=1)
* np.mean(v), 0.,
2. * np.std(isc - i, ddof=1) * np.mean(isc - i)],
[0., 0., np.std(v, ddof=1) * np.std(isc - i, ddof=1), 0.,
0.],
[0., 0., 0., np.std(v, ddof=1) ** 2., 0.],
[0., 0., 0., 0., np.std(isc - i, ddof=1) ** 2.]])
# translate from coefficients in rotated space (gamma) to coefficients in
# original coordinates (beta)
beta = np.linalg.lstsq(np.dot(mb, ma), gamma[0:5], rcond=None)[0]
return beta