Source code for pvops.iv.extractor
"""
Derive the effective diode parameters from a set of input curves.
"""
import numpy as np
import matplotlib.pyplot as plt
import scipy
import sklearn
from pvops.iv.simulator import Simulator
import time
from pvops.iv.physics_utils import iv_cutoff, T_to_tcell, \
calculate_IVparams, smooth_curve
[docs]
class BruteForceExtractor():
'''Process measured IV curves to extract diode parameters.
Requires a set of curves to create Isc vs Irr and Voc vs Temp vs Isc(Irr)
Parameters
----------
input_df : DataFrame
Contains IV curves with a datetime index
current_col : string
Indicates column where current values in IV curve are located;
each cell is an array of current values in a single IV curve
voltage_col : string
Indicates column where voltage values in IV curve are located;
each cell is an array of voltage values in a single IV curve
irradiance_col : string
Indicates column where irradiance value (W/m2)
temperature_col : string
Indicates column where temperature value (C)
T_type : string
Describe input temperature, either 'ambient' or 'module' or 'cell'
'''
def __init__(
self,
input_df,
current_col,
voltage_col,
irradiance_col,
temperature_col,
T_type,
windspeed_col=None,
Simulator_mod_specs=None,
Simulator_pristine_condition=None):
self.Simulator_mod_specs = Simulator_mod_specs
self.Simulator_pristine_condition = Simulator_pristine_condition
self.tstamps = input_df.index.tolist()
self.Is = input_df[current_col].tolist()
self.Vs = input_df[voltage_col].tolist()
self.Irrs = input_df[irradiance_col].tolist()
self.Temps = input_df[temperature_col].tolist()
self.T_type = T_type
self.Tcs = []
if self.T_type == 'ambient' and windspeed_col is None:
raise Exception(
"Wind speed must be specified if passing ambient temperature so that the cell temperature can be derived.")
if windspeed_col is not None:
self.WSs = input_df[windspeed_col].tolist()
if self.T_type == 'ambient':
for irr, temp, ws in zip(self.Irrs, self.Temps, self.WSs):
Tc = T_to_tcell(irr, temp, ws, self.T_type)
self.Tcs.append(Tc)
if self.T_type == 'module':
for irr, temp in zip(self.Irrs, self.Temps):
Tc = T_to_tcell(irr, temp, [], self.T_type)
self.Tcs.append(Tc)
self.measured_info = []
for i in range(len(self.Is)):
Varray = self.Vs[i]
Iarray = self.Is[i]
Irr = self.Irrs[i]
T = self.Temps[i]
self.measured_info.append({"V": Varray, "I": Iarray, "E": Irr, "T": T})
self.n_samples = len(input_df.index)
self.params = {}
[docs]
def create_string_object(self, iph, io, rs, rsh, nnsvth):
# TODO write docstring
kwargs = {}
if self.Simulator_mod_specs is not None:
kwargs.update({'mod_specs': self.Simulator_mod_specs})
if self.Simulator_pristine_condition is not None:
kwargs.update(
{'pristine_condition': self.Simulator_pristine_condition})
kwargs.update({'replacement_5params': {'I_L_ref': iph,
'I_o_ref': io,
'R_s': rs,
'R_sh_ref': rsh,
'a_ref': nnsvth}
})
sim = Simulator(**kwargs)
# set new defaults
for sample_i, sample in enumerate(self.measured_info):
condition = {'identifier': f'case_{self.counter}_{sample_i}',
'E': sample['E'],
'Tc': sample['T']
}
sim.add_preset_conditions(
'complete', condition, save_name=f'mod_case_{self.counter}_{sample_i}')
if isinstance(self.n_mods, int):
if self.n_mods > 1:
sim.build_strings({f'str_case_{self.counter}_{sample_i}': [
f'mod_case_{self.counter}_{sample_i}'] * self.n_mods})
elif self.n_mods != 1:
raise Exception(
f"Input a valid number of modules, n_mods. You inputted {self.n_mods}")
# elif isinstance(self.n_mods, (tuple, list, np.ndarray)):
# sim.build_strings({f'str_case_{self.counter}_{sample_i}': [
# f'mod_case_{self.counter}_{sample_i}']*self.n_mods[0] + ['pristine'] * (self.n_mods[1]-self.n_mods[0])})
else:
raise ValueError(
f"Expected n_mods to be a integer. Got: {type(self.n_mods)}")
start_t = time.time()
sim.simulate()
if self.verbose >= 2:
print(
f'\tSimulations completed after {round(time.time() - start_t, 2)} seconds')
return sim
[docs]
def f_multiple_samples(self, params):
# TODO write docstring
iph, io, rs, rsh, nnsvth = params
if self.user_func is None:
sim = self.create_string_object(self, iph, io, rs, rsh, nnsvth)
else:
sim = self.user_func(self, iph, io, rs, rsh, nnsvth)
msse_tot = 0
if self.verbose >= 2:
perc_diff = 100 * \
(np.array(params) - np.array(self.start_conds)) / \
np.array(self.start_conds)
meas_Iscs = []
meas_Vocs = []
meas_Pmps = []
sim_Iscs = []
sim_Vocs = []
sim_Pmps = []
for sample_i, sample in enumerate(self.measured_info):
if self.n_mods > 1:
Varr = sim.multilevel_ivdata['string'][f'str_case_{self.counter}_{sample_i}']['V'][0]
Iarr = sim.multilevel_ivdata['string'][f'str_case_{self.counter}_{sample_i}']['I'][0]
elif self.n_mods == 1:
Varr = sim.multilevel_ivdata['module'][f'mod_case_{self.counter}_{sample_i}']['V'][0]
Iarr = sim.multilevel_ivdata['module'][f'mod_case_{self.counter}_{sample_i}']['I'][0]
# resample to same voltage domain as measured
simI_interp = np.interp(sample['V'], Varr, Iarr)
msse = sklearn.metrics.mean_squared_error(sample['I'], simI_interp)
msse_tot += msse
if self.verbose >= 2:
Vco, Ico = iv_cutoff(Varr, Iarr, 0)
sim_params = calculate_IVparams(Vco, Ico)
meas_params = calculate_IVparams(sample['V'], sample['I'])
meas_Iscs.append(meas_params['isc'])
meas_Vocs.append(meas_params['voc'])
meas_Pmps.append(meas_params['pmp'])
sim_Iscs.append(sim_params['isc'])
sim_Vocs.append(sim_params['voc'])
sim_Pmps.append(sim_params['pmp'])
if self.verbose >= 2:
minpmps_m = min(min(meas_Pmps), min(sim_Pmps))
maxpmps_m = max(max(meas_Pmps), max(sim_Pmps))
plt.plot(meas_Pmps, sim_Pmps, 'go')
plt.plot(list(range(int(minpmps_m - 10), int(maxpmps_m + 10 + 1))),
list(range(int(minpmps_m - 10), int(maxpmps_m + 10 + 1))), 'b--')
plt.title('Measured v. Simulated Pmpp')
plt.xlabel('Measured (W)')
plt.ylabel('Simulated (W)')
plt.xlim(minpmps_m - 5, maxpmps_m + 5)
plt.ylim(minpmps_m - 5, maxpmps_m + 5)
plt.show()
minvocs_m = min(min(meas_Vocs), min(sim_Vocs))
maxvocs_m = max(max(meas_Vocs), max(sim_Vocs))
plt.plot(meas_Vocs, sim_Vocs, 'ro')
plt.plot(list(range(int(minvocs_m - 10), int(maxvocs_m + 10 + 1))),
list(range(int(minvocs_m - 10), int(maxvocs_m + 10 + 1))), 'b--')
plt.title('Measured v. Simulated Voc')
plt.xlabel('Measured (V)')
plt.ylabel('Simulated (V)')
plt.xlim(minvocs_m - 5, maxvocs_m + 5)
plt.ylim(minvocs_m - 5, maxvocs_m + 5)
plt.show()
miniscs_m = min(min(meas_Iscs), min(sim_Iscs))
maxiscs_m = max(max(meas_Iscs), max(sim_Iscs))
plt.plot(meas_Iscs, sim_Iscs, 'ko')
plt.plot(list(range(int(miniscs_m - 0.5), int(maxiscs_m + 0.5 + 2))),
list(range(int(miniscs_m - 0.5), int(maxiscs_m + 0.5 + 2))), 'b--')
plt.title('Measured v. Simulated Isc')
plt.xlabel('Measured (A)')
plt.ylabel('Simulated (A)')
plt.xlim(miniscs_m - 0.5, maxiscs_m + 0.5)
plt.ylim(miniscs_m - 0.5, maxiscs_m + 0.5)
plt.show()
plt.plot(sample['V'], simI_interp, 'r', label='Simulated')
plt.title("SIMULATED")
plt.show()
plt.plot(sample['V'], simI_interp, 'r', label='Simulated')
plt.plot(sample['V'], sample['I'], 'k', label='Measured')
plt.legend()
plt.xlabel('Voltage (V)')
plt.ylabel('Current (A)')
plt.title(
f'One example: case {self.counter} with % Diff.: {perc_diff}')
plt.show()
print('Params used in ^ iteration: ', params)
self.counter += 1
self.msses.append(msse_tot)
return msse_tot
[docs]
def fit_params(self, cell_parameters, n_mods, bounds_func, user_func=None, verbose=0):
"""
Fit diode parameters from a set of IV curves.
Parameters
----------
cell_parameters : dict
Cell-level parameters, usually extracted from the CEC
database, which will be used as the
initial guesses in the optimization process.
n_mods : int
if int, defines the number of modules in a
string(1=simulate a single module)
bounds_func : function
Function to establish the bounded search space
See below for an example:
.. code-block:: python
def bounds_func(iph,io,rs,rsh,nnsvth,perc_adjust=0.5):
return ((iph - 0.5*iph*perc_adjust, iph + 2*iph*perc_adjust),
(io - 40*io*perc_adjust, io + 40*io*perc_adjust),
(rs - 20*rs*perc_adjust, rs + 20*rs*perc_adjust),
(rsh - 150*rsh*perc_adjust, rsh + 150*rsh*perc_adjust),
(nnsvth - 10*nnsvth*perc_adjust, nnsvth + 10*nnsvth*perc_adjust))
user_func : function
Optional, a function similar to `self.create_string_object`
which has the following inputs:
`self, iph, io, rs, rsh, nnsvth`. This can be used to
extract unique failure parameterization.
verbose : int
if verbose >= 1, print information about fitting
if verbose >= 2, plot information about each iteration
"""
self.user_func = user_func
self.verbose = verbose
self.n_mods = n_mods
self.g = 1000
self.t = 25
self.cell_parameters = cell_parameters
self.counter = 0
self.msses = []
iph = cell_parameters['I_L_ref']
io = cell_parameters['I_o_ref']
rs = cell_parameters['R_s']
rsh = cell_parameters['R_sh_ref']
nnsvth = cell_parameters['a_ref']
self.start_conds = (iph, io, rs, rsh, nnsvth)
bounds = bounds_func(*self.start_conds)
if self.verbose >= 1:
print('Given 5params:', iph, io, rs, rsh, nnsvth)
converged_solution = scipy.optimize.minimize(self.f_multiple_samples,
(iph, io, rs, rsh, nnsvth),
bounds=bounds,
method='TNC')
if self.verbose >= 1:
print('bounds', bounds)
print('initial: ', (iph, io, rs, rsh, nnsvth))
print('solution: ', converged_solution)
return converged_solution['x']