Source code for solcore.poisson_drift_diffusion.QWunit

import numpy as np
from scipy.special import erfc
from solcore import constants, material, si
from solcore.structure import Layer, Structure
from solcore.quantum_mechanics import schrodinger
from solcore.quantum_mechanics.kp_bulk import kp8x8_bulk
from solcore.quantum_mechanics.strain import strain_calculation_parameters
from solcore.absorption_calculator import adachi_alpha
import sys

q = constants.q
pi = constants.pi
h = constants.h
kb = constants.kb
m0 = constants.electron_mass
vacuum_permittivity = constants.vacuum_permittivity


[docs]class QWunit(Structure): """ Asembles a group of layers as a quantum well, calculating its properties as a whole - It needs a minimum of 3 layers - There must be exactly one layer with the role = "well". - Top and bottom layers are assumed to be the barriers. - Anything not a barrier not a well will be considered as "interlayer". Since the Poisson - Drift-Diffusion solver can not work with superlatices, there is no point of considering the solution of more than one QW. All QWs entering into the solver are independent, regardless of the width of the barriers. To account for the possible presence of nearby QWs, perdiodic boundary conditions can be used to solve the Schrodinger equation. If barriers + interlayers are thick enought, this make no difference but if they are thin, it affects the number of confined levels. It does not 'create' minibands, though, so the results with thin barriers must be used with caution. """ def __init__(self, *args, **kwargs): """ Initialises the QWunit, checking that it has all the necessary layers """ super().__init__(*args, **kwargs) if len(self) < 3: print("ERROR creating a QWunit: a minimum of 3 layers is necesarry to make a unit. Only {0} found. ".format( len(self))) sys.exit() self.QW_number = 0 self.QW_width = 0 self[0].__dict__["role"] = "barrier" self[-1].__dict__["role"] = "barrier" for index in range(1, len(self) - 1): layer = self[index] if self[index].role in ["well", "qw", "QW", "Qw", "Well"]: self.QW_number += 1 self.QW_width = self[index].width self[index].role = "well" else: self[index].__dict__["role"] = "interlayer" if self.QW_number != 1: print("ERROR creating a QWunit: There must be exactly one layer with the role = 'well'. {0} found. ".format( self.QW_number)) sys.exit() if 'substrate' not in kwargs.keys(): print("ERROR creating a QWunit: There must be a substrate defined with the keyword 'substrate'. ") sys.exit() # We update the structure of labels since that is what the quantum mechanics modules uses to identify the regions, not the role. for index in range(len(self)): self.labels[index] = self[index].role # ------
[docs] def RecalculateKP(self, index, mode): """ Recalculates the band structure of the QW unit to account for modifications due to strain. This is already done in the Schrodinger solver, but it does not produce that information as output. We do it so here. """ if mode == 'kp8x8_bulk': kp_result = kp8x8_bulk(material=self[index].material, host_material=self.substrate, averaging_points=6) c, hh, lh, so, me, mhh, mlh, mso = kp_result self[index].material.eff_mass_electron_Gamma = me self[index].material.eff_mass_hh_z = mhh self[index].material.eff_mass_lh_z = mlh # We recalculate the bandgap adn the electron afinity Ec = self[index].material.valence_band_offset + self[index].material.band_gap self[index].band_gap = c - max(hh, lh) self[index].electron_affinity = self[index].material.electron_affinity - c + Ec else: me = self[index].material.eff_mass_electron_Gamma * m0 mhh = self[index].material.eff_mass_hh_z * m0 mlh = self[index].material.eff_mass_lh_z * m0 # Idem strain_parameters = strain_calculation_parameters(self.substrate, self[index].material, should_print=False) self[index].electron_affinity = self[index].material.electron_affinity - strain_parameters.delta_Ec self[index].band_gap = self[index].material.band_gap + strain_parameters.delta_Ec + max( strain_parameters.delta_Ehh, strain_parameters.delta_Elh) # Calculating this properly for QWs is done in the "RecalculateDensityOfStates" routine self[index].material.__dict__["Ncc"] = 2 * (2 * pi * me * kb * self.T / h ** 2) ** 1.5 self[index].material.__dict__["Nvhh"] = 2 * (2 * pi * mhh * kb * self.T / h ** 2) ** 1.5 self[index].material.__dict__["Nvlh"] = 2 * (2 * pi * mlh * kb * self.T / h ** 2) ** 1.5
# ------
[docs] def RecalculateBandEdges(self, mode, SR): """The effective bandgap and effective affinity of each layer depends on the energy level, e.g.: the minimum energy for electrons is not the band edge of the conduction band, but the ground level. In this function we modify that creating an effective electron afinity and band gaps for all layers in the QW. # # For the barriers, the electron afinity and band gap is the same than in bulk, modified by the kp calculation, if necesary.""" for index in range(len(self)): self.RecalculateKP(index, mode) self[index].__dict__["elevels"] = [] self[index].__dict__["hhlevels"] = [] self[index].__dict__["lhlevels"] = [] self[index].__dict__["eff_band_gap"] = self[index].band_gap self[index].__dict__["eff_electron_affinity"] = self[index].electron_affinity if self.labels[index] is "well": self[index].elevels = self.elevels self[index].hhlevels = self.hhlevels self[index].lhlevels = self.lhlevels # We create an effective bandgap and electron afinitiy to account for the confinement energies of electrons and holes self[index].eff_band_gap = self[index].eff_band_gap + self.elevels[0] * q self[index].eff_electron_affinity = self[index].eff_electron_affinity - self.elevels[0] * q # if ( max(schrodinger_result["potentials"]["Vhh"]) > max(schrodinger_result["potentials"]["Vlh"]) ): if (SR["E"]["Ehh"][0] > SR["E"]["Elh"][0]): self[index].eff_band_gap = self[index].eff_band_gap + self.hhlevels[0] * q else: self[index].eff_band_gap = self[index].eff_band_gap + self.lhlevels[0] * q well_eff_electron_afinity = self[index].eff_electron_affinity well_eff_band_gap = self[index].eff_band_gap # We re-run the loop to correct the electron affinity of the interlayers for index, layer in enumerate(self): if self.labels[index] is "interlayer": # The effective electron afinity is whatever is lower, its own afinity or the effective affinity of the well self[index].eff_electron_affinity = min(self[index].eff_electron_affinity, well_eff_electron_afinity) # The bandgap is corrected so the valence band in the interlayer is always equal or lower than in the QW gap_offset = (well_eff_electron_afinity + well_eff_band_gap) - ( self[index].eff_electron_affinity + self[index].eff_band_gap) if gap_offset > 0: self[index].eff_band_gap = self[index].eff_band_gap + gap_offset
# ------
[docs] def RecalculateDensityOfStates(self): """ Calculates the effective density of states for each layer in the QW. The general rule is: # 1- Barriers have the bulk density of states # 2- QW have ALL the density of states asociated with the confined states + bulk density of states above the barrier # 3- Interlayers have only the bulk density of states above the barrier # # This simplification is similar to that in J. Nelson, M. Paxman, K. W. J. Barnham, J. S. Roberts, and C. Button, “Steady-state carrier escape from single quantum wells,” IEEE J. Quantum Electron., vol. 29, no. 6, pp. 1460–1468, 1993. # # From a physical porint of view, it can certainly be done much better""" # We use the average barrier electron afinity (considering only the barriers at the ends) as the electron afinity of the barrier Xb = (self[0].electron_affinity + self[-1].electron_affinity) / 2 Egb = (self[0].band_gap + self[-1].band_gap) / 2 b = q / (kb * self.T) for index in range(len(self)): Ncc = self[index].material.Ncc Nvhh = self[index].material.Nvhh Nvlh = self[index].material.Nvlh if self.labels[index] is "barrier": # Barriers have the bulk density of states self[index].material.__dict__["Nc"] = Ncc self[index].material.__dict__["Nv"] = Nvhh + Nvlh elif self.labels[index] is "interlayer": # For electrons ------------------------------------------------------------- Vconf = self[index].electron_affinity - Xb self[index].material.__dict__["Nc"] = Ncc * erfc((b * Vconf) ** 0.5) # For holes ------------------------------------------------------------------ Vconf = Xb + Egb - self[index].electron_affinity - self[index].band_gap self[index].material.__dict__["Nv"] = (Nvhh + Nvlh) * erfc((b * Vconf) ** 0.5) else: # For electrons ------------------------------------------------------------- # 1- Contribution from bulk: Vconf = self[index].electron_affinity - Xb self[index].material.__dict__["Nc"] = Ncc * erfc((b * Vconf) ** 0.5) # 2- Contribution from confined levels Nqw = 0 for eLevel in self[index].elevels: Nqw = Nqw + np.exp(-b * eLevel) Nqw = 2. / self[index].width * (Ncc / 2.) ** (2. / 3.) * Nqw self[index].material.Nc = self[index].material.Nc + Nqw # For holes ------------------------------------------------------------------ # 1- Contribution from bulk: Vconf = Xb + Egb - self[index].electron_affinity - self[index].band_gap self[index].material.__dict__["Nv"] = (Nvhh + Nvlh) * erfc((b * Vconf) ** 0.5) # 2- Contribution from heavy holes confined levels Nqw = 0 for hhLevel in self[index].hhlevels: Nqw = Nqw + (Nvhh / 2.) ** (2. / 3.) * np.exp(-b * hhLevel) # 3- Contribution from light holes confined levels for lhLevel in self[index].lhlevels: Nqw = Nqw + (Nvlh / 2.) ** (2. / 3.) * np.exp(-b * lhLevel) Nqw = 2. / self[index].width * Nqw self[index].material.Nv = self[index].material.Nv + Nqw
# ------
[docs] def CalculateAbsorption(self, use_Adachi, SR): """ If required, this function calculates the absorption of the QW, putting together the absorption of the confined levels and the absorption of the bulk. As with the density of states, the rules are: # # 1- Barriers have the bulk absorption # 2- Interlayers have the bulk absorption from the barrier energy and zero below that # 3- Wells have the absorption of the confined levels below the barrier energy and of the bulk above it. The calculation is similar to: C. I. Cabrera, J. C. Rimada, J. P. Connolly, and L. Hernandez, “Modelling of GaAsP/InGaAs/GaAs strain-balanced multiple-quantum well solar cells,” J. Appl. Phys., vol. 113, no. 2, p. 024512, Jan. 2013.""" edge = (self[0].band_gap + self[-1].band_gap) / 2 / q edge_index = np.abs(self.wl - 1240e-9 / edge).argmin() # We set the energy levels and absorption coefficient of each layer of the QW unit. self.absorption = 0 * self.wl for index in range(len(self)): if use_Adachi: try: self[index].material.absorption = \ adachi_alpha.create_adachi_alpha(SolcoreMaterialToStr(self[index].material), T=self.T, wl=self.wl)[ 3] # 0 = Energy, 1 = n, 2 = k, 3 = Absorption except: self[index].material.absorption = self[index].material.alpha(self.wl) # self[index].material.absorption[self.wl>1240e-9/sc.asUnit(self[index].material.band_gap, 'eV' )] = 0 else: try: self[index].material.absorption = self[index].material.alpha(self.wl) # self[index].material.absorption[self.wl>1240e-9/sc.asUnit(self[index].material.band_gap, 'eV' )] = 0 except: print("Warning: Using Adachi calculation to estimate the absorption coefficient of layer: ", self[index]) self[index].material.absorption = \ adachi_alpha.create_adachi_alpha(SolcoreMaterialToStr(self[index].material), T=self.T, wl=self.wl)[ 3] # 0 = Energy, 1 = n, 2 = k, 3 = Absorption # self[index].material.absorption[ edge_index: ] = 0 if self.labels[index] is "well": self[index].material.absorption = self[index].material.absorption - self[index].material.absorption[ edge_index] self[index].material.absorption[edge_index:] = 0 self[index].material.absorption[self[index].material.absorption < 0] = 0 self[index].material.absorption = self[index].material.absorption + SR["alpha"][1] * self[ index].width / self.QW_width elif self.labels[index] is "interlayer": self[index].material.absorption[edge_index:] = 0 else: self[index].material.absorption[edge_index:] = 0 self.absorption += self[index].material.absorption
# ------
[docs] def solve(self, Efield=0, WLsteps=(300e-9, 1100e-9, 201), wavelengths=None, periodic=True, filter_strength=0.0, blur=None, blurmode="left", offset=0, mode='kp8x8_bulk', use_Adachi=False, calculate_absorption=True, alpha_params=None, T=293): """ Solves the structure, calculating the energy levels, the absorption, etc. """ self.T = T if calculate_absorption: # If we want the absorption but have not provided enough data for that, we make up some default values if alpha_params == None: if wavelengths is None: self.wl = np.linspace(*WLsteps) else: self.wl = wavelengths E = 1240 / (self.wl * 1e9) * q alpha_params = { "well_width": self.QW_width, "theta": 0, "eps": 12.9 * vacuum_permittivity, "espace": E, "hwhm": si("4meV"), "dimensionality": 0.2, # "line_shape": "Gauss" } else: self.wl = 1240 / (alpha_params["espace"] * 1e-9) * q SR, bands = schrodinger(self, mode=mode, periodic=True, calculate_absorption=calculate_absorption, Efield=Efield, blur=None, blurmode="left", alpha_params=alpha_params, filter_strength=filter_strength) self.__dict__["elevels"] = (SR["E"]["Ee"][:] - min(SR["potentials"]["Ve"])) / q self.__dict__["hhlevels"] = (max(SR["potentials"]["Vhh"]) - SR["E"]["Ehh"][:]) / q self.__dict__["lhlevels"] = (max(SR["potentials"]["Vlh"]) - SR["E"]["Elh"][:]) / q self.RecalculateBandEdges(mode, SR) self.RecalculateDensityOfStates() if calculate_absorption: self.CalculateAbsorption(use_Adachi, SR) return SR
# ------ # ------
[docs]def SolcoreMaterialToStr(material_input): """Translate a solcore material composition into a string that is what the Adachi calculator needs.""" material_string = material_input.__str__().strip('<>').split(" ") material_name = material_string[0].strip("'") composition = {'material': material_name} if len(material_name) > 4: material_composition = material_string[2].split("=") for i, comp in enumerate(material_composition): if comp in material_name: composition['element'] = material_composition[i] composition['fraction'] = float(material_composition[i + 1]) output = "%s {'%s':%s}" % (composition['material'], composition['element'], composition['fraction']) return output