"""Provides general functionality for Agent objects for non ideal diode simulations"""
######### Package Imports #########################################################################
import os, uuid, sys, copy, warnings
import numpy as np
import pandas as pd
from scipy import interpolate, constants
try:
import pvlib
from pvlib.pvsystem import i_from_v
got_pvlib = True
except:
got_pvlib = False
warnings.warn('pvlib not installed, using scipy for diode equation')
from optimpv import *
from optimpv.general.general import calc_metric, loss_function, transform_data
from optimpv.general.BaseAgent import BaseAgent
from optimpv.Diodefits.DiodeModel import *
## Physics constants
q = constants.value(u'elementary charge')
eps_0 = constants.value(u'electric constant')
kb = constants.value(u'Boltzmann constant in eV/K')
######### Agent Definition #######################################################################
[docs]
class DiodeAgent(BaseAgent):
"""Agent object for non ideal diode simulations
with the following formula:
J = Jph - J0*[exp(-(V-J*R_series)/(n*Vt*)) - 1] - (V - J*R_series)/R_shunt
see optimpv.Diodefits.DiodeModel.py for more details
Parameters
----------
params : list of Fitparam() objects
List of Fitparam() objects.
X : array-like
1-D or 2-D array containing the voltage (1st column) and if specified the Gfrac (2nd column) values.
y : array-like
1-D array containing the current values.
T : float, optional
Temperature in K, by default 300.
exp_format : str or list of str, optional
Format of the experimental data, by default 'light'.
metric : str or list of str, optional
Metric to evaluate the model, see optimpv.general.calc_metric for options, by default 'mse'.
loss : str or list of str, optional
Loss function to use, see optimpv.general.loss_function for options, by default 'linear'.
threshold : int or list of int, optional
Threshold value for the loss function used when doing multi-objective optimization, by default 100.
minimize : bool or list of bool, optional
If True then minimize the loss function, if False then maximize the loss function (note that if running a fit minize should be True), by default True.
yerr : array-like or list of array-like, optional
Errors in the current values, by default None.
weight : array-like or list of array-like, optional
Weights used for fitting if weight is None and yerr is not None, then weight = 1/yerr**2, by default None.
tracking_metric : str or list of str, optional
Additional metrics to track and report in run_Ax output, by default None.
tracking_loss : str or list of str, optional
Loss functions to apply to tracking metrics, by default None.
tracking_exp_format : str or list of str, optional
Experimental formats for tracking metrics, by default None.
tracking_X : array-like or list of array-like, optional
X values for tracking metrics, by default None.
tracking_y : array-like or list of array-like, optional
y values for tracking metrics, by default None.
tracking_weight : array-like or list of array-like, optional
Weights for tracking metrics, by default None.
name : str, optional
Name of the agent, by default 'diode'.
use_pvlib : bool, optional
If True then use the pvlib library to calculate the diode equation, by default False.
**kwargs : dict
Additional keyword arguments.
"""
def __init__(self, params, X, y, T = 300, exp_format = 'light', metric = 'mse', loss = 'linear',
threshold = 100, minimize = True, yerr = None, weight = None,
tracking_metric = None, tracking_loss = None, tracking_exp_format = None,
tracking_X = None, tracking_y = None, tracking_weight = None,
name = 'diode', use_pvlib = False, **kwargs):
# super().__init__(**kwargs)
self.params = params
self.X = X # voltage and Gfrac
self.y = y
self.T = T # temperature in K
# Convert single values to lists for consistency
if isinstance(exp_format, str):
self.exp_format = [exp_format]
else:
self.exp_format = exp_format
if isinstance(metric, str):
self.metric = [metric]
else:
self.metric = metric
if isinstance(loss, str):
self.loss = [loss]
else:
self.loss = loss
if isinstance(threshold, (int, float)):
self.threshold = [threshold]
else:
self.threshold = threshold
if isinstance(minimize, bool):
self.minimize = [minimize]
else:
self.minimize = minimize
self.yerr = [yerr]
# Handle weight calculation from yerr if needed
if weight is None and yerr is not None:
self.weight = [1/np.asarray(yerr)**2]
else:
self.weight = [weight]
self.name = name
self.use_pvlib = use_pvlib
self.kwargs = kwargs
# Initialize tracking parameters
self.tracking_metric = tracking_metric
self.tracking_loss = tracking_loss
self.tracking_exp_format = tracking_exp_format
self.tracking_X = tracking_X
self.tracking_y = tracking_y
self.tracking_weight = tracking_weight
# Process tracking metrics and losses
if self.tracking_metric is not None:
if isinstance(self.tracking_metric, str):
self.tracking_metric = [self.tracking_metric]
if self.tracking_loss is None:
self.tracking_loss = ['linear'] * len(self.tracking_metric)
elif isinstance(self.tracking_loss, str):
self.tracking_loss = [self.tracking_loss] * len(self.tracking_metric)
# Ensure tracking_metric and tracking_loss have the same length
if len(self.tracking_metric) != len(self.tracking_loss):
raise ValueError('tracking_metric and tracking_loss must have the same length')
# Process tracking_exp_format
if self.tracking_exp_format is None:
# Default to the main experiment formats if not specified
self.tracking_exp_format = self.exp_format
elif isinstance(self.tracking_exp_format, str):
self.tracking_exp_format = [self.tracking_exp_format]
# check that all elements in tracking_exp_format are valid
for form in self.tracking_exp_format:
if form not in ['dark', 'light']:
raise ValueError(f'{form} is an invalid tracking_exp_format, must be either "dark" or "light"')
# Process tracking_X and tracking_y
# Check if all tracking formats are in main exp_format
all_formats_in_main = all(fmt in self.exp_format for fmt in self.tracking_exp_format)
if self.tracking_X is None or self.tracking_y is None:
if not all_formats_in_main:
raise ValueError('tracking_X and tracking_y must be provided when tracking_exp_format contains formats not in exp_format')
# Construct tracking_X and tracking_y from main X and y based on matching formats
self.tracking_X = []
self.tracking_y = []
for fmt in self.tracking_exp_format:
fmt_indices = [i for i, main_fmt in enumerate(self.exp_format) if main_fmt == fmt]
if fmt_indices:
# Use the first matching format's data
idx = fmt_indices[0]
self.tracking_X.append(self.X)
self.tracking_y.append(self.y)
# Ensure tracking_X and tracking_y are lists
if not isinstance(self.tracking_X, list):
self.tracking_X = [self.tracking_X]
if not isinstance(self.tracking_y, list):
self.tracking_y = [self.tracking_y]
# Check that tracking_X and tracking_y have the right lengths
if len(self.tracking_X) != len(self.tracking_exp_format) or len(self.tracking_y) != len(self.tracking_exp_format):
raise ValueError('tracking_X and tracking_y must have the same length as tracking_exp_format')
# Process tracking_weight
if self.tracking_weight is None and all_formats_in_main:
# Use the main weights if available
self.tracking_weight = []
if all_formats_in_main:
for fmt in self.tracking_exp_format:
fmt_indices = [i for i, main_fmt in enumerate(self.exp_format) if main_fmt == fmt]
if fmt_indices:
idx = fmt_indices[0]
self.tracking_weight.append(self.weight[idx])
else:
self.tracking_weight.append(None)
else:
self.tracking_weight = [None] * len(self.tracking_exp_format)
elif not isinstance(self.tracking_weight, list):
self.tracking_weight = [self.tracking_weight]
# Ensure tracking_weight has the right length
if len(self.tracking_weight) != len(self.tracking_exp_format):
raise ValueError('tracking_weight must have the same length as tracking_exp_format')
# check that all elements in exp_format are valid
for form in self.exp_format:
if form not in ['dark','light']:
raise ValueError(f'{form} is an invalid exp_format, must be either "dark" or "light"')
self.all_agent_metrics = self.get_all_agent_metric_names()
self.all_agent_tracking_metrics = self.get_all_agent_tracking_metric_names()
# Add compare_type parameter
self.compare_type = self.kwargs.get('compare_type', 'linear')
if 'compare_type' in self.kwargs.keys():
self.kwargs.pop('compare_type')
# Validate compare_type
if self.compare_type not in ['linear', 'log', 'normalized', 'normalized_log', 'sqrt']:
raise ValueError('compare_type must be either linear, log, normalized, normalized_log, or sqrt')
if got_pvlib == False:
self.use_pvlib = False
[docs]
def run(self,parameters):
"""Run the diode model and calculate the loss function
Parameters
----------
parameters : dict
Dictionary of parameter names and values.
Returns
-------
float
Loss function value.
"""
# check that all the arguments are in the parameters dictionary
arg_names = ['J0','n','R_series','R_shunt']
if self.exp_format[0] == 'light':
arg_names.append('Jph')
# for arg in arg_names:
# if arg not in parameters.keys():
# raise ValueError('Parameter: {} not in parameters dictionary'.format(arg))
if 'T' not in parameters.keys():
T_ = self.T
else:
T_ = parameters['T']
parameters_rescaled = self.params_rescale(parameters, self.params)
if self.use_pvlib and got_pvlib:
print('Using pvlib to calculate diode equation')
nVt = parameters_rescaled['n']*kb*T_
if self.exp_format[0] == 'dark':
J = -i_from_v(self.X, 0, parameters_rescaled['J0'], parameters_rescaled['R_series'], parameters_rescaled['R_shunt'], nVt)
elif self.exp_format[0] == 'light':
J = -i_from_v(self.X, parameters_rescaled['Jph'], parameters_rescaled['J0'], parameters_rescaled['R_series'], parameters_rescaled['R_shunt'], nVt)
else:
if self.exp_format[0] == 'dark':
J = NonIdealDiode_dark(self.X, parameters_rescaled['J0'], parameters_rescaled['n'], parameters_rescaled['R_series'], parameters_rescaled['R_shunt'], T = T_)
elif self.exp_format[0] == 'light':
J = NonIdealDiode_light(self.X, parameters_rescaled['J0'], parameters_rescaled['n'], parameters_rescaled['R_series'], parameters_rescaled['R_shunt'], parameters_rescaled['Jph'], T = T_)
return J
[docs]
def run_Ax(self,parameters):
"""Run the diode model and calculate the loss function
Parameters
----------
parameters : dict
Dictionary of parameter names and values.
Returns
-------
float
Loss function value.
"""
if self.exp_format[0] == 'light':
self.compare_logs = self.kwargs.get('compare_logs',False)
else:
self.compare_logs = self.kwargs.get('compare_logs',True)
yfit = self.run(parameters) # run the diode model
dum_dict = {}
for i in range(len(self.exp_format)):
metric_name = self.metric[i]
# Apply data transformation based on compare_type
if self.compare_type == 'linear' and self.compare_logs:
epsilon = np.finfo(np.float64).eps
# if 0 in yfit, then add epsilon to avoid log(0)
yfit_trans = yfit.copy()
yfit_trans[abs(yfit_trans) <= epsilon] = epsilon
y_trans = copy.deepcopy(self.y)
y_trans[abs(y_trans) <= epsilon] = epsilon
metric_value = calc_metric(np.log10(abs(y_trans)), np.log10(abs(yfit_trans)),
sample_weight=self.weight[i], metric_name=metric_name)
elif self.compare_type != 'linear':
# Use the transform_data function for non-linear transforms
y_trans, yfit_trans = transform_data(
self.y, yfit, transform_type=self.compare_type
)
metric_value = calc_metric(y_trans, yfit_trans,
sample_weight=self.weight[i], metric_name=metric_name)
else:
# Standard linear comparison
metric_value = calc_metric(self.y, yfit,
sample_weight=self.weight[i], metric_name=metric_name)
dum_dict[self.all_agent_metrics[i]] = loss_function(metric_value, loss=self.loss[i])
# Calculate tracking metrics if they exist
if self.tracking_metric is not None:
for j in range(len(self.all_agent_tracking_metrics)):
if self.tracking_exp_format[j] == self.exp_format[i]:
tracking_metric_name = self.tracking_metric[j]
# Apply the same transform as the main metric
if self.compare_type == 'linear' and self.compare_logs:
tracking_metric_value = calc_metric(np.log10(abs(y_trans)), np.log10(abs(yfit_trans)),
sample_weight=self.tracking_weight[j], metric_name=tracking_metric_name)
elif self.compare_type != 'linear':
tracking_metric_value = calc_metric(y_trans, yfit_trans,
sample_weight=self.tracking_weight[j], metric_name=tracking_metric_name)
else:
tracking_metric_value = calc_metric(self.tracking_y[j], yfit,
sample_weight=self.tracking_weight[j], metric_name=tracking_metric_name)
dum_dict[self.all_agent_tracking_metrics[j]] = loss_function(
tracking_metric_value, loss=self.tracking_loss[j])
return dum_dict