def update_units_labels_and_values(self) -> None: """ Updates the x units labels and fields :return: None """ # If x units haven't changed, we do nothing new_x = self.units.get().split('_')[-1] old_x = self.units_x.get() if new_x == old_x: return self.units_x.set(new_x) old_min = self.x_min.get() old_max = self.x_max.get() if all(t in ('nm', 'eV') for t in (old_x, new_x)): new_min = eVnm(old_min) new_max = eVnm(old_max) elif all(t in ('nm', 'J') for t in (old_x, new_x)): new_min = nmJ(old_min) new_max = nmJ(old_max) elif all(t in ('nm', 'm') for t in (old_x, new_x)): factor = 1e-9 if old_x == 'nm' else 1e9 new_min = factor * old_min new_max = factor * old_max elif all(t in ('nm', 'hz') for t in (old_x, new_x)): new_min = nmHz(old_min) new_max = nmHz(old_max) elif all(t in ('m', 'J') for t in (old_x, new_x)): new_min = mJ(old_min) new_max = mJ(old_max) elif all(t in ('m', 'eV') for t in (old_x, new_x)): factor = h * c / q new_min = factor / old_min new_max = factor / old_max elif all(t in ('m', 'hz') for t in (old_x, new_x)): factor = c new_min = factor / old_min new_max = factor / old_max elif all(t in ('J', 'eV') for t in (old_x, new_x)): factor = q if old_x == 'eV' else 1 / q new_min = factor * old_min new_max = factor * old_max elif all(t in ('J', 'hz') for t in (old_x, new_x)): factor = 1 / h if old_x == 'J' else h new_min = factor * old_min new_max = factor * old_max else: # eV <-> hz factor = q / h if old_x == 'eV' else h / q new_min = factor * old_min new_max = factor * old_max # Now we have to check if maximum and minimum are in the correct order, reversing them, otherwise if new_min > new_max: new_min, new_max = new_max, new_min self.x_min.set(format(new_min, '.4')) self.x_max.set(format(new_max, '.4'))
def _get_power_density_per_eV(self, energy): """ Function that returns the spectrum in power density per eV. :param energy: Array with the energies at which to calculate the spectrum (in eV) :return: The spectrum in the chosen units. """ wavelength = eVnm(energy)[::-1] output = self._spectrum(wavelength) energy_eV, output = spectral_conversion_nm_ev(wavelength, output) return output
def photon_flux_per_ev(spectrum: Callable[[np.ndarray], np.ndarray], x: np.ndarray): """ Function that returns the spectrum in photon flux per eV. The input spectrum is assumed to be in power density per nanometer. :param spectrum: The spectrum to interpolate. :param x: Array with the energies (in eV) :return: The spectrum in the chosen units. """ wavelength = eVnm(x)[::-1] output = spectrum(wavelength) _, output = spectral_conversion_nm_ev(wavelength, output) return output / (q * x)
else: return False else: relative = max(a, b) / min(a, b) - 1. if relative < relative_precision: return True else: return False def independent_nm_ev(x, y): return UnitsSystem().eVnm(x)[::-1], y[::-1] def independent_nm_J(x, y): return UnitsSystem().nmJ(x)[::-1], y[::-1] def independent_m_J(x, y): return reverse(UnitsSystem().mJ(x)), reverse(y) def reverse(x): return x[::-1] if __name__ == '__main__': UnitsSystem() print(solcore.eVnm(1240))
def test_units_correctly_calculated(): a_nm = 1239.8417166827828 assert a_nm == approx(solcore.eVnm(1))
def test_03_units_correctly_calculated(self): a_nm = 1239.8417166827828 self.assertAlmostEqual(a_nm, solcore.eVnm(1))
def calculate_junction_sr(junc, energies, bs, bs_initial, V, printParameters=False): """ Calculates the total quantum efficiency, the QE splitted by regions, photocurrent and other parameters for a given junction at a given voltage. :param junc: The junction object :param energies: The energies at which to perform the calculation :param bs: The spectral power density reaching the junction :param bsInitial: The initial power density :param V: The voltage at which to perform the calculations (not implemented, yet) :param printParameters: If a list of all parameters must be printed :return: """ # First we have to figure out if we are talking about a PN, NP, PIN or NIP junction sn = 0 if not hasattr(junc, "sn") else junc.sn sp = 0 if not hasattr(junc, "sp") else junc.sp # First we search for the emitter and check if it is n-type or p-type idx = 0 pn_or_np = 'pn' for layer in junc: if layer.role is not 'emitter': idx += 1 else: Na = 0 Nd = 0 if hasattr(layer.material, 'Na'): Na = layer.material.Na if hasattr(layer.material, 'Nd'): Nd = layer.material.Nd if Na < Nd: pn_or_np = "np" nRegion = junc[idx] else: pRegion = junc[idx] break # Now we check for an intrinsic region and, if there is, for the base. if junc[idx + 1].role is 'intrinsic': iRegion = junc[idx + 1] if junc[idx + 2].role is 'base': if pn_or_np == "pn": nRegion = junc[idx + 2] else: pRegion = junc[idx + 2] else: raise RuntimeError( 'ERROR processing junctions: A layer following the "intrinsic" layer must be defined as ' '"base".') # If there is no intrinsic region, we check directly the base elif junc[idx + 1].role is 'base': if pn_or_np == "pn": nRegion = junc[idx + 1] else: pRegion = junc[idx + 1] iRegion = None else: raise RuntimeError( 'ERROR processing junctions: A layer following the "emitter" must be defined as "intrinsic"' 'or "base".') # With all regions identified, it's time to start doing calculations T = nRegion.material.T kbT = kb * T Egap = nRegion.material.band_gap xp = pRegion.width xn = nRegion.width xi = 0 if iRegion is None else iRegion.width # Now we have to get all the material parameters needed for the calculation if hasattr(junc, "dielectric_constant"): es = junc.dielectric_constant else: es = nRegion.material.permittivity * vacuum_permittivity # equal for n and p. I hope. # For the diffusion lenght, subscript n and p refer to the carriers, electrons and holes if hasattr(junc, "ln"): ln = junc.ln else: ln = pRegion.material.electron_diffusion_length if hasattr(junc, "lp"): lp = junc.lp else: lp = nRegion.material.hole_diffusion_length # For the diffusion coefficient, n and p refer to the regions, n side and p side. Yeah, it's confusing... if hasattr(junc, "mup"): dp = junc.mup * kb * T / q else: dp = pRegion.material.electron_mobility * kb * T / q if hasattr(junc, "mun"): dn = junc.mun * kb * T / q else: dn = nRegion.material.hole_mobility * kb * T / q # Effective masses and effective density of states mEff_h = nRegion.material.eff_mass_hh_z * electron_mass mEff_e = pRegion.material.eff_mass_electron * electron_mass Nv = 2 * (mEff_h * kb * T / (2 * pi * hbar**2))**1.5 # Jenny p58 Nc = 2 * (mEff_e * kb * T / (2 * pi * hbar**2))**1.5 niSquared = Nc * Nv * np.exp(-Egap / (kb * T)) ni = np.sqrt(niSquared) Na = pRegion.material.Na Nd = nRegion.material.Nd Vbi = (kb * T / q) * np.log(Nd * Na / niSquared) if not hasattr( junc, "Vbi") else junc.Vbi # Jenny p146 # And now we account for the possible applied voltage, which can be, at most, equal to Vbi V = min(Vbi, V) Vbi = Vbi - V # It's time to calculate the depletion widths if not hasattr(junc, "wp") or not hasattr(junc, "wn"): if hasattr(junc, "depletion_approximation" ) and junc.depletion_approximation == "one-sided abrupt": print( "using one-sided abrupt junction approximation for depletion width" ) science_reference( "Sze abrupt junction approximation", "Sze: The Physics of Semiconductor Devices, 2nd edition, John Wiley & Sons, Inc (2007)" ) wp = np.sqrt(2 * es * Vbi / (q * Na)) wn = np.sqrt(2 * es * Vbi / (q * Nd)) else: wn = (-xi + np.sqrt(xi**2 + 2. * es * Vbi / q * (1 / Na + 1 / Nd))) / (1 + Nd / Na) wp = (-xi + np.sqrt(xi**2 + 2. * es * Vbi / q * (1 / Na + 1 / Nd))) / (1 + Na / Nd) wn = wn if not hasattr(junc, "wn") else junc.wn wp = wp if not hasattr(junc, "wp") else junc.wp # we have an array of alpha values that needs to be interpolated to the right energies alphaN = nRegion.material.alphaE( energies) # create numpy array at right energies. alphaP = pRegion.material.alphaE( energies) # create numpy array at right energies. alphaI = iRegion.material.alphaE(energies) if iRegion else 0 depleted_width = wn + wp + xi bs_incident_on_top = bs # Now it is time to calculate currents if pn_or_np == "pn": bs_incident_on_bottom = bs * np.exp(-alphaP * xp - alphaN * wn - xi * alphaI) bs_incident_on_depleted = bs * np.exp(-alphaP * (xp - wp)) alphaTop = alphaP alphaBottom = alphaN l_top, l_bottom = ln, lp x_top, x_bottom = xp, xn w_top, w_bottom = wp, wn s_top, s_bottom = sp, sn d_top, d_bottom = dp, dn min_top, min_bot = niSquared / Na, niSquared / Nd else: bs_incident_on_bottom = bs * np.exp(-alphaN * xn - alphaP * wp - xi * alphaI) bs_incident_on_depleted = bs * np.exp(-alphaN * (xn - wn)) alphaTop = alphaN alphaBottom = alphaP l_bottom, l_top = ln, lp x_bottom, x_top = xp, xn w_bottom, w_top = wp, wn s_bottom, s_top = sp, sn d_bottom, d_top = dp, dn min_bot, min_top = niSquared / Na, niSquared / Nd j_top, JtopDark = get_j_top(x_top, w_top, l_top, s_top, d_top, alphaTop, bs_incident_on_top, V, min_top, T) j_bottom, JbotDark = get_j_bot(x_bottom, w_bottom, l_bottom, s_bottom, d_bottom, alphaBottom, bs_incident_on_bottom, V, min_bot, T) jgen = q * bs_incident_on_depleted * ( 1 - np.exp(-alphaI * xi - alphaN * wn - alphaP * wp) ) # jgen. Jenny, p. 159 # hereby we define the subscripts to refer to the layer in which the current is generated: if pn_or_np == "pn": jn, jp = j_bottom, j_top JnDark, JpDark = JbotDark, JtopDark else: jp, jn = j_bottom, j_top JpDark, JnDark = JbotDark, JtopDark # These might not be the right lifetimes. Actually, they are not as they include all recombination processes, not # just SRH recombination, which is what the equation in Jenny, p159 refers to. Let´ leave them, for now. lifetime_n = ln**2 / dn lifetime_p = lp**2 / dp # Jenny p163 # Jrec. Jenny, p.159. Note capital J. This does not need integrating over energies Jrec = q * ni * (wn + wp + xi) / np.sqrt( lifetime_n * lifetime_p) * np.sinh(q * V / (2 * kbT)) / (q * Vbi / kbT) * pi # jgen = q* bs*(1 - exp(-depleted_width*alpha))*exp(-(xn-wn)*alpha); nDepletionCharge = wn * Nd * q pDepletionCharge = wp * Na * q Vbi2 = (0.5 * (wn + wp) + xi) * pDepletionCharge / es good_indeces = np.isfinite(jn) * np.isfinite(jp) * np.isfinite(jgen) energies = energies[good_indeces] jn = jn[good_indeces] jp = jp[good_indeces] jgen = jgen[good_indeces] # jn[jn < 0] = 0 # jp[jp < 0] = 0 # jgen[jgen < 0] = 0 bs_initial = bs_initial[good_indeces] Jn = np.trapz(y=jn, x=energies) Jp = np.trapz(y=jp, x=energies) Jgen = np.trapz(y=jgen, x=energies) if printParameters: jSum = list((jn + jp + jgen) / bs_initial / q) peakQE = max(jSum) BandgapEV = convert(Egap, "J", "eV") peakQEE = convert(energies[jSum.index(peakQE)], "J", "eV") parameterDictionary = { "typee": "PIN" if iRegion is not None else "PN", "Na": Na, "Nd": Nd, "mEff_e": mEff_e, "mEff_h": mEff_h, "dp": dp, "dn": dn, "T": T, "es": es, "Vbi": Vbi, "xpUm": convert(xp, "m", "um"), "xnUm": convert(xn, "m", "um"), "BandgapEV": BandgapEV, "BandgapNM": eVnm(BandgapEV), "pMatrialString": str(pRegion.material), "nMatrialString": str(nRegion.material), "relativeEffectiveMassE": mEff_e / electron_mass, "relativeEffectiveMassH": mEff_h / electron_mass, "wnNm": convert(wn, "m", "nm"), "wpNm": convert(wp, "m", "nm"), "NaCM": convert(Na, "m-3", "cm-3"), "NdCM": convert(Nd, "m-3", "cm-3"), "permittivity": es / vacuum_permittivity, "peakQE": peakQE * 100, "peakQEE": peakQEE, "peakQENM": eVnm(peakQEE), "lpum": convert(lp, "m", "um"), "lnum": convert(ln, "m", "um"), "nQ": convert(nDepletionCharge, "m-2", "cm-2"), "pQ": convert(pDepletionCharge, "m-2", "cm-2"), "pDepletionCharge": pDepletionCharge, "nDepletionCharge": nDepletionCharge, "fieldMax": pDepletionCharge / (es), "iRegionString": "", "Vbi2": Vbi2, "ni": ni } if iRegion is not None: parameterDictionary["iRegionString"] = """ | i region: {xiUm:.2f} um {iMatrialString} | field: {fieldMax:.3e} V/m""".format( **{ "iMatrialString": str(iRegion.material), "xiUm": convert(xi, "m", "um"), "fieldMax": pDepletionCharge / (es), }) print("""\n | Calculating {typee} QE. Active Parameters: | | p region: {xpUm:.2f} um {pMatrialString} | Na = {Na:.3e} m-3 ({NaCM:.3e} cm-3) | minority carrier (electron) diffusion length: {lpum:.3f} um | minority carrier (electron) effective mass: {relativeEffectiveMassE:.3f} (relative) {mEff_e:.3e} kg (absolute) | minority carrier (electron) diffusivity = {dp:.3e} m2/s | depletion width: {wpNm:.3f} nm | charge in depletion region: {pDepletionCharge:.3e} C m-2 ({pQ:.3e} C cm-2){iRegionString} | n region: {xnUm:.2f} um {nMatrialString} | Nd = {Nd:.3e} m-3 ({NdCM:.3e} cm-3) | minority carrier (hole) diffusion length: {lnum:.3f} um | minority carrier (hole) effective mass: {relativeEffectiveMassH:.3f} (relative) {mEff_h:.3e} kg (absolute) | minority carrier (hole) diffusivity = {dn:.3e} m2/s | depletion width: {wnNm:.3f} nm | charge in depletion region: {nDepletionCharge:.3e} C m-2 ({nQ:.3e} C cm-2) | Bandgap: {BandgapEV:.3f} eV ({BandgapNM:.2f} nm) | Temperature: {T:.2f} K | permittivity: {permittivity:.3f} (relative) {es:.3e} A s V-1 m-1 (absolute) | built-in Voltage: {Vbi:.3f} V | peak field: {fieldMax:.3e} V/m | ni: {ni:e} | Result: | \tPeak QE = {peakQE:.1f} % at {peakQEE:.3f} eV ({peakQENM:.2f} nm)""".format( **parameterDictionary)) return { "qe_n": jn / q / bs_initial, "qe_p": jp / q / bs_initial, "qe_scr": jgen / q / bs_initial, "qe_tot": (jn + jp + jgen) / q / bs_initial, "Jn_sc": Jn, "Jp_sc": Jp, "Jscr_sc": Jgen, "Jn_dif": JnDark, "Jp_dif": JpDark, "Jscr_srh": Jrec, "J": (Jn + Jp + Jgen - Jrec - JnDark - JpDark), "e": energies, "Temporary locals dictionary for radiative efficiency": locals() }