Modeling of photovoltaic devices

This ex­am­ple demon­strates mod­el­ing of pho­to­volta­ic de­vices. Fig­ures of mer­it, such as fill fac­tor, are ex­tract­ed from re­sults of the sim­u­la­tion. For il­lus­tra­tion, a ref­er­ence is par­tial­ly re­pro­duced.

Sim­u­la­tions like those shown above can show pos­si­ble op­ti­miza­tion opor­tu­ni­ties for de­vices. They al­so can yield ul­ti­mate per­for­mance of ide­al­ized de­vices.

%matplotlib inline
import matplotlib.pylab as plt
from oedes import *
init_notebook()
import scipy.interpolate
plt.rcParams['axes.formatter.useoffset']=False

Model

We start with pop­u­lar as­sump­tion that light is ab­sorbed uni­form­ly in­side the de­vice. The as­sump­tion can be jus­ti­fied for thin de­vices, and for white il­lu­mi­na­tion.

def absorption(x):
    return 1.5e28

The base mod­el con­sists in Pois­son’s equa­tion cou­pled to the drift-dif­fu­sion equa­tions for elec­trons and holes. Con­stant mo­bil­i­ties are as­sumed. Ad­di­tion­al­ly, con­tacts can be de­fined as se­lec­tive (block­ing elec­trons and holes at “in­valid” elec­trode”), or non-se­lec­tive with lo­cal ther­mal equi­lib­ri­um is as­sumed at any elec­trode for all charge car­ri­ers.

def base_model(L=50e-9,selective=False,**kwargs):
    model = models.BaseModel()
    mesh = fvm.mesh1d(L)
    models.std.bulk_heterojunction(model, mesh, absorption=absorption,selective_contacts=selective,**kwargs)
    model.setUp()
    return model

The ba­sic mod­el cre­at­ed by func­tion above con­tains no re­com­bi­na­tion term, and must be su­ple­ment­ed with it. Com­plete mod­els are cre­at­ed by func­tions be­low, with the fol­low­ing op­tions for the re­com­bi­na­tion mod­el:

  • di­rect : R=\beta \left(n p-n_i p_i \right)
  • Langevin: R=\frac{\mu_n+\mu_p}{\varepsilon} \left(n p-n_i p_i \right)
  • Shock­ley-Reed-Hall re­com­bi­na­tion, in par­al­lel with di­rect re­com­bi­na­tion

Ab­sorp­tion is as­sumed to cre­ate free elec­trons and holes di­rect­ly.

def model_Langevin(**kwargs):
    return base_model(langevin_recombination=True,**kwargs)
def model_const(**kwargs):
    return base_model(const_recombination=True,**kwargs)
def model_SRH(**kwargs):
    return base_model(const_recombination=True,srh_recombination=True,**kwargs)

Be­low is a pro­ce­dure gen­er­at­ing de­fault sim­u­la­tion pa­ram­e­ters. They are parametrized by the bandgap, by the (sym­met­ric) bar­ri­er at the elec­trodes, and by SRH life­time. Note that not all pa­ram­e­ters are used at the same time, for ex­am­ple SRH pa­ram­e­ters are not used by non-SRH mod­els.

def make_params(barrier=0.3,bandgap=1.2,srh_tau=1e-8):
    params=models.std.bulk_heterojunction_params(barrier=barrier,bandgap=bandgap,Nc=1e27,Nv=1e27)
    srh_trate=1e-8
    params.update({
        'beta':7.23e-17,
        'electron.srh.trate':srh_trate,
        'hole.srh.trate':srh_trate,
        'srh.N0':1./(srh_tau*srh_trate),
        'srh.energy':-bandgap*0.5
    })
    return params

Calculations

The func­tion be­low takes I_V curve, which should in­clude points V=0 and J=0, and cal­cu­lates the open cir­cuit volt­age, the pow­er at max­i­mum pow­er point, and the fill fac­tor.

def performance(v,J):
    iv=scipy.interpolate.InterpolatedUnivariateSpline(v,J)
    Isc=iv(0.)
    Voc,=iv.roots()
    v=np.linspace(0,Voc)
    Pmax=np.amax(-v*iv(v))
    Ff=-Pmax/(Voc*Isc)
    return dict(Ff=Ff,Voc=Voc,Isc=Isc,Pmax=Pmax)

In the ref­er­ence, the mo­bil­i­ties of elec­trons and holes are var­ied but kept equal. The fol­low­ing shows how such sweep can be im­ple­ment­ed.

mu_values = np.logspace(-10,-2,19)
def mu_sweep(params):
    for mu in mu_values:
        p=dict(params)
        p['electron.mu']=mu
        p['hole.mu']=mu
        yield mu,p
v_sweep = sweep('electrode0.voltage',np.linspace(0.,0.8,40))

Be­cause dif­fer­ent mod­els are con­sid­ered be­low, a com­mon func­tion is de­fined here to run the sim­u­la­tion and to plot the re­sult. The func­tion takes mod­el as an ar­gu­ment.

def Voc_Ff(model,params):
    c=context(model)
    result=[]
    def onemu(mu, cmu):
        for _ in cmu.sweep(cmu.params, v_sweep):
            pass
        v,J=cmu.teval(v_sweep.parameter_name,'J')
        p = performance(v,J)
        return (mu, p['Voc'], p['Ff'])
    result = np.asarray([onemu(*_) for _ in c.sweep(params, mu_sweep)])
    testing.store(result)
    fig,(ax_voc,ax_ff)=plt.subplots(nrows=2,sharex=True)
    ax_voc.plot(result[:,0],result[:,1])
    ax_ff.plot(result[:,0],result[:,2])
    ax_ff.set_xlabel(r'$\mu \mathrm{[m^2 V^{-1} s^{-1}]}$')
    ax_ff.set_xscale('log')
    ax_ff.set_ylabel('FF')
    ax_voc.set_ylabel('$V_{oc}$');
    return result
params=make_params()

Results

Direct recombination, non-selective contacts

As seen be­low, in the case of di­rect re­com­bi­na­tion, se­lec­tive con­tacts are use­ful for im­prov­ing fill fac­tor and open-cir­cuit volt­age re­gard­less of mo­bil­i­ties.

Voc_Ff(model_const(selective=False),params);

Direct recombination, selective contacts

Voc_Ff(model_const(selective=True),params);

Langevin recombination, non-selective contants

If Langevin re­com­bi­na­tion is as­sumed, open cir­cuit volt­age drops re­gard­less of con­tact se­lec­tiv­i­ty.

Voc_Ff(model_Langevin(selective=False),params);

Langevin recombination, selective contants

Voc_Ff(model_Langevin(selective=True),params);

SRH recombination, non-selective contacts

The case of SRH re­com­bi­na­tion re­sem­bles the case of di­rect re­com­bi­na­tion in its de­pen­dence on mo­bil­i­ty. This is not sur­pris­ing, as in both cas­es the mo­bil­i­ty does not en­ter the re­com­bi­na­tion term R .

Voc_Ff(model_SRH(selective=False),params);

SRH recombination, selective contacts

Voc_Ff(model_SRH(selective=True),params);

Reference

Wolf­gang Tress, Karl Leo, Moritz Riede, Op­ti­mum mo­bil­i­ty, con­tact prop­er­ties, and open-cir­cuit volt­age of or­gan­ic so­lar cells: A drift-dif­fu­sion sim­u­la­tion study, Phys. Rev. B. 85155201 (2012))