import numpy as np
import scipy
import math
from sklearn.linear_model import LinearRegression
import copy
[docs]
def calculate_IVparams(v, c):
"""Calculate parameters of IV curve.
This needs to be reworked: extrapolate parameters from linear region instead of
hardcoded regions.
Parameters
----------
x : numpy array
X-axis data
y : numpy array
Y-axis data
npts : int
Optional, number of points to resample curve
deg : int
Optional, polyfit degree
Returns
-------
Dictionary of IV curve parameters
"""
isc_lim = 0.1
voc_lim = 0.01
# maximum power point
pmax = np.max((v * c))
mpp_idx = np.argmax((v * c))
vpmax = v[mpp_idx]
ipmax = c[mpp_idx]
# for snippet_idx in range(len(v[::5])):
# isc and rsh
if isinstance(isc_lim, float):
isc_size = int(len(c) * isc_lim)
else:
isc_size = isc_lim
isc_lm = LinearRegression().fit(
v[:isc_size].reshape(-1, 1), c[:isc_size].reshape(-1, 1))
isc = isc_lm.predict(np.asarray([0]).reshape(-1, 1))[0][0]
rsh = 1 / (isc_lm.coef_[0][0] * -1)
# voc and rs
if isinstance(voc_lim, float):
voc_size = int(len(v) * voc_lim)
else:
voc_size = voc_lim
voc_lm = LinearRegression().fit(c[::-1][:voc_size].reshape(-1, 1),
v[::-1][:voc_size].reshape(-1, 1))
voc = voc_lm.predict(np.asarray([0]).reshape(-1, 1))[0][0]
rs = voc_lm.coef_[0][0] * -1
# fill factor
ff = (ipmax * vpmax) / (isc * voc)
return {
'pmp': pmax,
'vmp': vpmax,
'imp': ipmax,
'voc': voc,
'isc': isc,
'rs': rs,
'rsh': rsh,
'ff': ff,
}
[docs]
def smooth_curve(x, y, npts=50, deg=12):
"""Smooth curve using a polyfit
Parameters
----------
x : numpy array
X-axis data
y : numpy array
Y-axis data
npts : int
Optional, number of points to resample curve
deg : int
Optional, polyfit degree
Returns
-------
smoothed x array
smoothed y array
"""
xx = np.linspace(1, np.max(x), npts)
yhat = np.poly1d(np.polyfit(x, y, deg))
yh = yhat(xx)
return xx, yh
[docs]
def iv_cutoff(Varr, Iarr, val):
"""Cut IV curve greater than voltage `val` (usually 0)
Parameters
----------
V: numpy array
Voltage array
I: numpy array
Current array
val: numeric
Filter threshold
Returns
-------
V_cutoff, I_cutoff
"""
msk = Varr > val
return Varr[msk], Iarr[msk]
[docs]
def intersection(x1, y1, x2, y2):
"""Compute intersection of curves, y1=f(x1) and y2=f(x2).
Adapted from https://stackoverflow.com/a/5462917
Parameters
----------
x1: numpy array
X-axis data for curve 1
y1: numpy array
Y-axis data for curve 1
x2: numpy array
X-axis data for curve 2
y2: numpy array
Y-axis data for curve 2
Returns
-------
intersection coordinates
"""
x1 = copy.copy(np.asarray(x1))
x2 = copy.copy(np.asarray(x2))
y1 = copy.copy(np.asarray(y1))
y2 = copy.copy(np.asarray(y2))
def _upsample_curve(Varr, Iarr, n_pts=1000):
vmax = Varr.max()
vnot = Varr.min()
resol = (vmax - vnot) / n_pts
v_interps = np.arange(vnot, vmax, resol)
return v_interps, np.interp(v_interps, Varr, Iarr)
x1, y1 = _upsample_curve(x1, y1)
x2, y2 = _upsample_curve(x2, y2)
def _rect_inter_inner(x1, x2):
n1 = x1.shape[0] - 1
n2 = x2.shape[0] - 1
X1 = np.c_[x1[:-1], x1[1:]]
X2 = np.c_[x2[:-1], x2[1:]]
S1 = np.tile(X1.min(axis=1), (n2, 1)).T
S2 = np.tile(X2.max(axis=1), (n1, 1))
S3 = np.tile(X1.max(axis=1), (n2, 1)).T
S4 = np.tile(X2.min(axis=1), (n1, 1))
return S1, S2, S3, S4
S1, S2, S3, S4 = _rect_inter_inner(x1, x2)
S5, S6, S7, S8 = _rect_inter_inner(y1, y2)
C1 = np.less_equal(S1, S2)
C2 = np.greater_equal(S3, S4)
C3 = np.less_equal(S5, S6)
C4 = np.greater_equal(S7, S8)
ii, jj = np.nonzero(C1 & C2 & C3 & C4)
n = len(ii)
dxy1 = np.diff(np.c_[x1, y1], axis=0)
dxy2 = np.diff(np.c_[x2, y2], axis=0)
T = np.zeros((4, n))
AA = np.zeros((4, 4, n))
AA[0:2, 2, :] = -1
AA[2:4, 3, :] = -1
AA[0::2, 0, :] = dxy1[ii, :].T
AA[1::2, 1, :] = dxy2[jj, :].T
BB = np.zeros((4, n))
BB[0, :] = -x1[ii].ravel()
BB[1, :] = -x2[jj].ravel()
BB[2, :] = -y1[ii].ravel()
BB[3, :] = -y2[jj].ravel()
for i in range(n):
try:
T[:, i] = np.linalg.solve(AA[:, :, i], BB[:, i])
except:
T[:, i] = np.Inf
in_range = (T[0, :] >= 0) & (T[1, :] >= 0) & (
T[0, :] <= 1) & (T[1, :] <= 1)
xy0 = T[2:, in_range]
xy0 = xy0.T
if not len(xy0[:, 1]):
import matplotlib.pyplot as plt
plt.figure(figsize=(13, 8))
plt.plot(x1, y1,
'bo', markersize=2, label='1')
plt.plot(x2, y2, 'ro',
markersize=2, label='2')
plt.legend()
plt.xlabel('V (Volts)')
plt.ylabel('I (Amps)')
# plt.ylim(0, max(max(y2),max(y1)) + 2.)
plt.ylim(-4, 20)
plt.xlim(-13.5, max(max(x2), max(x1)) + 2.)
plt.show()
print("x1 = ", list(x1))
print("x2 = ", list(x2))
print("y1 = ", list(y1))
print("y2 = ", list(y2))
return xy0[:, 0], xy0[:, 1]
[docs]
def T_to_tcell(POA, T, WS, T_type, a=-3.56, b=-0.0750, delTcnd=3):
''' Ambient temperature to cell temperature according to NREL
weather-correction. See :cite:t:`osti_1078057`.
Parameters
----------
Tamb: numerical,
Ambient temperature, in Celcius
WS: numerical,
Wind speed at height of 10 meters, in m/s
a, b, delTcnd: numerical,
Page 12 in :cite:p:`osti_1078057`
T_type: string,
Describe input temperature, either 'ambient' or 'module'
Returns
-------
numerical
Cell temperature, in Celcius
'''
Gstc = 1000
if T_type == 'ambient':
Tm = POA * np.exp(a + b * WS) + T
Tcell = Tm + (POA / Gstc) * delTcnd
elif T_type == 'module':
Tcell = T + (POA / Gstc) * delTcnd
return Tcell
def _aggregate_vectors(current_1, current_2):
if current_2 is not None:
return np.flipud(np.sort(np.unique(np.append(current_1, current_2))))
else:
return current_1
[docs]
def bypass(voltage, v_bypass):
''' Limits voltage to greater than -v_bypass.
Parameters
----------
voltage : numeric
Voltage for IV curve [V]
v_bypass : float or None, default None
Forward (positive) turn-on voltage of bypass diode, e.g., 0.5V [V]
Returns
-------
voltage : numeric
Voltage clipped to greater than -v-bpass
'''
return voltage.clip(min=-v_bypass)
[docs]
def add_series(voltage_1, current_1, voltage_2=None, current_2=None, v_bypass=None):
''' Adds two IV curves in series.
Parameters
----------
voltage_1 : numeric
Voltage for first IV curve [V]
current_1 : numeric
Current for first IV curve [A]
voltage_2 : numeric or None, default None
Voltage for second IV curve [V]
current_1 : numeric or None, default None
Voltage for second IV curve [A]
v_bypass : float or None, default None
Forward (positive) turn-on voltage of bypass diode, e.g., 0.5V [V]
Returns
-------
voltage : numeric
Voltage for combined IV curve [V]
current : numeric
Current for combined IV curve [V]
Note
----
Current for the combined IV curve is the sorted union of the current of the
two input IV curves. At current values in the other IV curve, voltage is
determined by linear interpolation. Voltage at current values outside an
IV curve's range is determined by linear extrapolation.
If `voltage_2` and `current_2` are None, returns `(voltage_1, current_1)`
to facilitate starting a loop over IV curves.
'''
if (voltage_2 is None) and (current_2 is None):
all_v, all_i = voltage_1, current_1
else:
all_i = _aggregate_vectors(current_1, current_2)
all_v = np.zeros_like(all_i)
f_interp1 = scipy.interpolate.interp1d(np.flipud(current_1), np.flipud(voltage_1),
kind='linear', fill_value='extrapolate')
all_v += f_interp1(all_i)
f_interp2 = scipy.interpolate.interp1d(np.flipud(current_2), np.flipud(voltage_2),
kind='linear', fill_value='extrapolate')
all_v += f_interp2(all_i)
if v_bypass:
all_v = bypass(all_v, v_bypass)
return all_v, all_i
[docs]
def voltage_pts(npts, v_oc, v_rbd):
'''Provide voltage points for an IV curve.
Points range from v_brd to v_oc, with denser spacing at both limits.
v=0 is included as the midpoint.
Based on method PVConstants.npts from pvmismatch
Parameters
----------
npts : integer
Number of points in voltage array.
v_oc : float
Open circuit voltage [V]
v_rbd : float
Reverse bias diode voltage (negative value expected) [V]
Returns
-------
array [V]
'''
npts_pos = npts // 2
if npts % 2:
npts_pos += 1
npts_neg = npts - npts_pos
# point spacing from 0 to 1, denser approaching 1
# decrease point spacing as voltage approaches Voc by using logspace
pts_pos = (11. - np.logspace(np.log10(11.), 0., npts_pos)) / 10.
pts_pos[0] = 0
pts_pos *= v_oc
pts_neg = (11. - np.logspace(np.log10(11.), 0., npts_neg)) / 10.
pts_neg = np.flipud(pts_neg)
pts_neg[0] -= 0.1 * (pts_neg[0] - pts_neg[1])
pts_neg *= v_rbd
pts = np.concatenate((pts_neg, pts_pos))
return pts
[docs]
def gt_correction(v, i, gact, tact, cecparams, n_units=1, option=3):
"""IV Trace Correction using irradiance and temperature.
Three correction options are provided, two of which are from an IEC standard.
Parameters
----------
v : numpy array
Voltage array
i : numpy array
Current array
gact : float
Irradiance upon measurement of IV trace
tact : float
Temperature (C or K) upon measuremnt of IV trace
cecparams : dict
CEC database parameters, as extracted by `pvops.iv.utils.get_CEC_params`.
n_units : int
Number of units (cells or modules) in string, default 1
option : int
Correction method choice. See method for specifics.
Returns
-------
vref
Corrected voltage array
iref
Corrected current array
"""
beta = cecparams['beta_oc'] # Voc temperature coefficient of Voc
alpha = cecparams['alpha_sc'] # Isc temperature coefficient of Isc
gref = 1000 # Reference Irradiance
tref = 50 # Reference temperature
beta *= n_units
if option in [1, 2]:
params = calculate_IVparams(v, i)
isc = params['isc']
voc = params['voc']
rs = params['rs']
# curve correction factor, k, must be derived
k1 = 0
k2 = 0
if option == 1:
# IEC60891 Procedure 1
iref = i + isc * ((gref / gact) - 1) + alpha * (tref - tact)
vref = v - rs * (iref - i) - k1 * iref * \
(tref - tact) + beta * (tref - tact)
elif option == 2:
# IEC60891 Procedure 2
iref = i * (1 + alpha * (tref - tact)) * (gref / gact)
vref = v + voc * (beta * (tref - tact) + alpha * math.log(gref / gact)
) - rs * (iref - i) - k2 * iref * (tref - tact)
elif option == 3:
vref = (v * (math.log10(gref) / math.log10(gact)) -
(beta * (tact - tref)))
iref = (i * (gref / gact)) - (alpha * (tact - tref))
return vref, iref