BOAR with SIMsalabim

Version 1.0.0 (c) Vincent M. Le Corre, Larry Lueer, i-MEET 2021-2023

This notebook is made to use BOAR in combination with drift-diffusion modeling to fit of ‘fake’ JV curves.
To perform the drift-diffusion simulation in the background we use the open-source program SIMsalabim, for more information about SIMsalabim please check the GitHub repository
Make sure you have SIMsalabim installed before running this Notebook.
Here we are fitting some ‘fake’ data that are generated by the drift-diffusion model.
[1]:
# Activate matplotlib widgets
# %matplotlib inline
# comment the next line if you are on the jupyterhub server
# %matplotlib widget
# %matplotlib notebook

# Import libraries
import sys,os
from numpy.random import default_rng
import warnings
warnings.filterwarnings('ignore') # comment this out to see warnings

# Import boar package
sys.path.append(os.path.abspath('../')) # add directory containing boar package to path
from boar import *

[2]:
# Define path to SIMsalabim
curr_dir = os.getcwd()
parent_dir = os.path.abspath(os.path.join(curr_dir, '..')) # path to the parent directory
path2simu = os.path.join(parent_dir, 'SIMsalabim','SimSS') # path to the SIMsalabim directory
# Directory where the results are stored
res_dir = os.path.join(curr_dir,'temp') # absolute path to the results directory (note that this will be delete in the last cell of this notebook)
dev_par_file = os.path.join(parent_dir,'Example_Data','Data_test','device_parameters_fake_OPV.txt') # absolute path to the device parameter file (here we use a fake OPV device for illustration purposes)

Define the free parameters to be optimized

[3]:
# define Fitparameters
True_params = {'kdirect':5e-18,'mun_0':2e-8,'mup_0':8e-8,'Nc':5e26,'Gehp':1.28e28,'W_L':0}#,'Rseries':3e-4,'Bulk_tr':1e20,'Gehp':1.28e28}
params = []

kdirect = Fitparam(name = 'kdirect', val = True_params['kdirect'] , relRange = 1.5, lims=[1e-18,1e-16],range_type='log',optim_type='log',display_name='k$_{2}$',unit='m$^{3}$ s$^{-1}$')
params.append(kdirect)

mun_0 = Fitparam(name = 'mun_0', val = True_params['mun_0'] , relRange = 1.5, lims=[1e-8,1e-7],range_type='log',optim_type='log',display_name='$\mu_n$',unit='m$^{2}$ V$^{-1}$ s$^{-1}$')
params.append(mun_0)

mup_0 = Fitparam(name = 'mup_0', val = True_params['mup_0'] , relRange = 1.5, lims=[1e-8,1e-7],range_type='log',optim_type='log',display_name='$\mu_p$',unit='m$^{2}$ V$^{-1}$ s$^{-1}$')
params.append(mup_0)

params_true = copy.deepcopy(params)

Check the structure of the device for SIMsalabim

Here we check the structure of the device for SIMsalabim and you can adjust the parameters if needed.

For example, if you want to change the thickness of the active layer (AL) you can do this by changing the value of the variable L in the cell below. To do so you need to update the value of the variable ParFileDic[‘L’] and set MakeUpdate to True.

By convention LTL is the ETL and RTL is the HTL.
Note that L is the total thickness of the ETL, HTL and active layer, so if you want to change the thickness of the active layer you need to subtract the thickness of the ETL and HTL from the total thickness. Such that:

\(L_{AL} = L - L_{LTL} - L_{RTL}\)

To check the name of the parameters in SIMsalabim please refer to the SIMsalabim documentation.

[4]:
# Vizualize the stack defined for the simulation
check_SIMsalabim_input = True
# from boar.SIMsalabim_utils.MakeDevParFile import *
# from boar.SIMsalabim_utils.GetInputPar import *

if check_SIMsalabim_input:
    from boar.SIMsalabim_utils.PlotInputPar import *
    # Load Device parameters file and update parameters
    ParFileDic = ReadParameterFile(dev_par_file) # read the parameters from the file

    # Change parameters in the dictionary
    ParFileDic['L'] =  100e-9

    MakeUpdate = False # set to True to update the parameters in the file
    if MakeUpdate:
        UpdateDevParFile(ParFileDic, path2simu) # update the parameters in the file

    # Visualize the parameters
    fig, axs = plt.subplots(2,2,figsize = (10,8))
    plot_input_nrj_diag(ParFileDic,ax=axs[0, 0])
    plot_input_mob(ParFileDic,ax=axs[0, 1])
    plot_input_dens(ParFileDic,ax=axs[1, 0])
    plot_input_SRH_lifetime(ParFileDic,ax=axs[1, 1],y_unit='ns',y2_unit='cm')
    plt.tight_layout()
    plt.show()
../_images/examples_DD_Fit_fake_OPV_7_0.png

Prepare fake data for fitting

In the next block we create some fake data with some random noise and plot it.

[5]:
# # create some fake data
Nc = 1 # number of fake datasets
V = np.linspace(0,1,100) # voltage
Gfrac = np.asarray([0.01,0.5,1]) # Gfrac, i.e. light intensity

X_dimensions = ['Vext','Gfrac'] # dimensions of the X array
X = np.array([[x,y] for y in Gfrac for x in V ] ) # X array
Xplot = Gfrac # X array for plotting

# Generate the fake data to fit
degradation_run = True
True_paramsList = []

# define the degradation of kdirect
kvals = np.geomspace(5e-18,5e-17,Nc) # simulate degradation of kdirect

dda = Drift_diffusion_agent(path2simu=path2simu) # instantiate the agent

ys = []
True_vals = []
True_FOMs = []
for kval in kvals:
    kdirect.val = kval
    True_paramsList.append({'kdirect':kval})
    True_params['kdirect'] = kval
    # store the true values for plotting later
    True_vals.append(True_params.copy())

    y = dda.DriftDiffusion_relative(X,params,X_dimensions=X_dimensions, max_jobs=3,dev_par_fname=dev_par_file) # simulate the data
    rng = default_rng()#
    noise = rng.standard_normal(y.shape) * 1.9
    #noise = noise * X[:,1] # try this: higher T - higher noise
    y+=noise # add some noise
    ys.append(y)

    plt.plot(X[:,0],y,'o')
plt.xlabel('Voltage [V]')
plt.ylabel('Current Density [A m$^{-2}$]')
# save params list to be modified later to store true values
params_true = copy.deepcopy(params)


../_images/examples_DD_Fit_fake_OPV_9_0.png

Start the optimization

[6]:
# Fit the datasets one by one
pf = [] # list to store the fit results
mo = MultiObjectiveOptimizer(params=params,res_dir=res_dir) # instantiate the optimizer

n_jobs = 4
n_jobs_init = 25
n_yscale=2
n_initial_points = 100
n_BO = 60
n_BO_warmstart = 60

for ii,y in enumerate(ys):
    target = {'model':partial(dda.DriftDiffusion_relative,X_dimensions=X_dimensions,max_jobs=3,dev_par_fname=dev_par_file),'target_name':'JV','data':{'X':X,'y':y,
                'X_dimensions':['Vext','Gfrac'],'X_units':['V','sun'],'y_dimension':'Current density','y_unit':r'$A/m²$'},'weight':1,'target_weight':1}
    mo.targets = [target]
    mo.params = params
    mo.warmstart = 'collect_init' if ii==0 else 'recall'
    # mo.warmstart = 'None'
    mo.SaveOldXY2file = os.path.join(res_dir,'old_XY.json') # path to the file where old points are saved
    mo.Path2OldXY = os.path.join(res_dir,'old_XY.json') # path to the file where old points are saved

    kwargs = {'check_improvement':'relax','max_loop_no_improvement':10,'xtol':1e-3,'ftol':1e-3}#,'initial_point_generator':'LHS'}
    kwargs_posterior = {'Nres':10,'gaussfilt':3,'logscale':True,'vmin':1e-100,'zoom':0,'min_prob':1e-40,'clear_axis':False,'True_values':True_vals[ii],'show_points':True,'savefig':False,'figname':'param_posterior'+str(ii),'full_grid':True,'randomize':False}
    kwargs_plot_obj = {'zscale':'linear'}

    r = mo.optimize_sko_parallel(n_jobs=n_jobs,n_yscale=n_yscale, n_BO=n_BO, n_initial_points = n_initial_points,n_BO_warmstart=n_BO_warmstart,n_jobs_init=n_jobs_init,kwargs=kwargs,verbose=False,loss='soft_l1',threshold=1000,base_estimator = 'GP',show_objective_func=False,show_posterior=True,kwargs_posterior = kwargs_posterior,kwargs_plot_obj=kwargs_plot_obj,)
    pf.append(deepcopy(mo.params)) # collects optimized fitparameters
    rrr = r['r'] # the results dict of the last optimizer.tell()


    #get true parameters and make a params object
    for param in params_true:
        if param.name in True_paramsList[ii]:
            param.val = True_paramsList[ii][param.name]

    # plot the fit results
    fit_results = []
    kwargs_plot_res = {'x_scaling':1,'xaxis_label':'Voltage [V]','xscale_type':'linear','y_scaling':1/10,'yaxis_label':'Current density [mA cm$^2$]','yscale_type':'linear','norm_data':False,'delog':False,'figsize':(10,10),'savefig':False,'figname':'JV_fits_' + str(ii),'figdir':'temp'}

    for num,t in enumerate(mo.targets):
        kwargs_plot_res['figname'] = os.path.join(res_dir,t['target_name']+f'_fit_{num}')
        dda.plot_fit_res(t,mo.params,'Vext',xlim=[],ylim=[],kwargs=kwargs_plot_res)

        X = t['data']['X']
        y = t['data']['y']
        X_dimensions = t['data']['X_dimensions']
        yfit = t['model'](X,mo.params,X_dimensions=X_dimensions) # get the best fits

        data = np.concatenate((X, y.reshape(len(y),1), yfit.reshape(len(yfit),1)), axis=1)
        fit_results.append(data)

    # prepare the data for saving
    param_dict = dda.get_param_dict(mo.params) # get fitparameters (and fixed ones) as dict
    pout = [[f'{v:.3E}' if isinstance(v,float) else v for _,v in pp.items()] for pp in param_dict] # convert to list of lists

    save_output = False
    if save_output:
        # produce output excel file with data, fitparameters and FOMs
        fn_xlsx = 'fits_results.xlsx'
        namecols = X_dimensions + ['Jexp','Jfit']
        # delete old file if it exists
        if os.path.exists(os.path.join(res_dir,fn_xlsx)):
            os.remove(os.path.join(res_dir,fn_xlsx))

        with pd.ExcelWriter(os.path.join(res_dir,fn_xlsx), mode='w') as writer:
            for i,t in enumerate(mo.targets):
                if 'target_name' in t.keys():
                    tname = t['target_name']
                else:
                    tname = 'data'
                namecols = X_dimensions + [tname+'_exp',tname+'_fit']
                df = pd.DataFrame(fit_results[i],columns=namecols)
                df.to_excel(writer, sheet_name = tname+f'_{i}')

            df = pd.DataFrame(pout,columns=[k for k in param_dict[0].keys()])
            df.to_excel(writer, sheet_name = f'params')

Starting with initial points
Initial points done in 11.37 s
Starting with BO
BO done in 146.52 s
Ground truth minimum at: [-17.3032731357855, -7.700143892096901, -7.091992000697518] with function value: 0.13506055772527503
Minimum of surrogate function: [-17.303273330240792, -7.700143956914993, -7.0919918671538245] with function value 0.13368151919531712
Sampling for posterior distribution done in  0.09167170524597168 s
../_images/examples_DD_Fit_fake_OPV_11_6.png
../_images/examples_DD_Fit_fake_OPV_11_7.png
[7]:
# plot the 2D posterior
mo.marginal_posterior_2D('mun_0','mup_0',points=mo.points,logscale=True,Nres=10,Ninteg=1e5,vmin=1e-40,True_values=True_vals[0])
../_images/examples_DD_Fit_fake_OPV_12_0.png
[8]:
mo.marginal_posterior_1D('mun_0',logscale=True,Nres=10,Ninteg=1e5,vmin=1e-40,True_values=True_vals[0])
../_images/examples_DD_Fit_fake_OPV_13_0.png
[8]:
(1.994601346585351e-08,
 (1.994601346585351e-08, 1.994601346585351e-08),
 [-18.0, -8.0, -8.0],
 [-16.0, -7.0, -7.0])
[9]:
# plot the parameters evolution
param_plot = dda.plot_params(pf,kwargs={'savefig':False,'figname':'plot_param','figdir':'temp','nrows':1,'ncols':3,'figsize':(10,10)})

../_images/examples_DD_Fit_fake_OPV_14_0.png
[10]:
# Clean output files from simulation folders
from boar.SIMsalabim_utils.CleanFolder import *

Do_Cleaning = True # Careful, this will delete all files in the folder
if Do_Cleaning:
    clean_up_output('tj',path2simu)
    clean_up_output('tVG',path2simu)
    clean_up_output('JV',path2simu)
    clean_up_output('Var',path2simu)
    clean_up_output('scPars',path2simu)
    clean_up_output('Str4Parallel',path2simu)
    clean_up_output('log',path2simu)
    # os.remove(mo.path2oldxy) # remove the old_xy.json file if it exists
    # delete warmstart folder if it exists
    if os.path.exists(os.path.join(os.getcwd(),'warmstart/')):
        shutil.rmtree(os.path.join(os.getcwd(),'warmstart/'))
    # delete temp folder if it exists
    if os.path.exists(res_dir):
        shutil.rmtree(res_dir)