Пример #1
0
    def Lorentz(self, energy, **kwargs):
        """
        Custom_CPPB.Lorentz() :: Classic Lorentz oscillator expression, taken from the J. A. Woollam ellipsometer
            manual.

        :param energy: Energy array (eV)
        :param kwargs: These take in individual parameters for the model. Keywords should take the following form;
                    Gamma
                    Alpha
                    E0
                    Amp

        :return: Lorentz oscillator contributions to the complex dielectric function.
        """

        # Scientific reference for this work...
        science_reference("J. A. Woollam, Guide to using WVASE",
                          "J. A. Woollam Co. Inc., Copyright 1994-2012")

        # Remove material_parameters argument from the kwargs dictionary to avoid confusion...
        if "material_parameters" in kwargs:
            del kwargs["material_parameters"]

        Gamma = self.Broad(kwargs["Gamma"], kwargs["Alpha"], kwargs["E0"],
                           energy)

        # Eps = (Params["C"] * Params["E0"]**2) / ((Params["E0"]**2 - energy**2) - 1j*energy*Gamma)
        Eps = (kwargs["Amp"] * kwargs["E0"]) / (kwargs["E0"]**2 - energy**2 -
                                                1j * Gamma * energy)

        # Additional line to address change in phase of the imaginary signal.
        return Eps.real + 1j * abs(Eps.imag)
def get_depletion_widths(junction, es, Vbi, V, Na, Nd, xi):

    if not hasattr(junction, "wp") or not hasattr(junction, "wn"):

        if hasattr(
                junction, "depletion_approximation"
        ) and junction.depletion_approximation == "one-sided abrupt":
            print(
                "using one-sided abrupt junction approximation for depletion width"
            )
            one_sided = True
        else:
            one_sided = False

        if one_sided:
            science_reference(
                "Sze abrupt junction approximation",
                "Sze: The Physics of Semiconductor Devices, 2nd edition, John Wiley & Sons, Inc (2007)"
            )
            wn = np.sqrt(2 * es * (Vbi - V) / (q * Nd))
            wp = np.sqrt(2 * es * (Vbi - V) / (q * Na))

        else:
            wn = (-xi + np.sqrt(xi**2 + 2. * es * (Vbi - V) / q *
                                (1 / Na + 1 / Nd))) / (1 + Nd / Na)
            wp = (-xi + np.sqrt(xi**2 + 2. * es * (Vbi - V) / q *
                                (1 / Na + 1 / Nd))) / (1 + Na / Nd)

    wn = wn if not hasattr(junction, "wn") else junction.wn
    wp = wp if not hasattr(junction, "wp") else junction.wp

    return wn, wp
def get_Jsrh(ni, V, Vbi, tp, tn, w, kbT, dEt=0):
    science_reference('SRH current term.',
                      'C. T. Sah, R. N. Noyce, and W. Shockley, “Carrier Generation and Recombination in P-N Junctions and P-N Junction Characteristics,” presented at the Proceedings of the IRE, 1957, vol. 45, no. 9, pp. 1228–1243.')

    out = np.zeros(V.shape)

    m = V >= -1120 * kbT / q
    out[m] = forward(ni, V[m], Vbi, tp, tn, w[m], kbT)

    return out
Пример #4
0
def exciton_bohr_radius(me, mh, eps):
    """
    :param me: electron effective mass (units: kg)
    :param mh: hole effective mass (units: kg)
    :param eps: dielectic constant (units: SI)
    :return: Exciton Borh radius
    """

    science_reference("Definition of the exciton bohr radius for a quantum well.",
                      "S. L. Chuang, Physics of Optoelectonic Devices, Second Edition, p.554, Table 13.1")
    mr = me * mh / (me + mh)  # reduced effective mass
    return hbar ** 2 / mr * (4 * np.pi * eps / q ** 2)
Пример #5
0
    def Sellmeier(self, energy, **kwargs):
        """
        Custom_CPPB.Sellmeier() :: Calculate the Sellmeier dispersion relation for the entirely real parts of the
            dielectric function. The Sellmeier relation is a sum of N terms.

        :param energy: Energy array (eV)
        :param kwargs: These take in individual parameters for the model. Parameters should be in units of "um".
            Keywords should take the following form;
                    An
                    Ln
                    NOTE: 'n' should take an integer from 1 to N where N is the number of Sellmeier terms.

        :return: Sellmeier contributions to the real part of the dielectric function.
        """

        # Scientific reference for this work...
        science_reference(
            "Wilhelm Sellmeier's development of the Cauchy dispersion relation, taken from the J."
            " A. Woollam WVASE manual",
            "J. A. Woollam Co. Inc., Copyright 1994-2012")

        # Remove material_parameters argument from the kwargs dictionary to avoid confusion...
        if "material_parameters" in kwargs:

            del kwargs["material_parameters"]

        # Work out number of Sellmeier terms from length of kwargs...
        if np.mod(len(kwargs), 2) != 0:

            raise ValueError(
                "Insufficient number of Sellmeier coefficients :: No. kwargs == %d"
                % len(kwargs))

        else:

            terms = len(kwargs) / 2

        # Define empty array for individual Sellmeier terms...
        epsilon = np.zeros((int(terms), len(energy)), dtype=complex)

        # define the conversion constant to go between energy in eV and lambda in SI units...
        C = ((const.h * const.c) / const.q) * 1E6

        # calculate Sellmeier expression for the given coefficients...
        for i in range(0, int(terms)):

            epsilon[i] = (kwargs["A%d" % (i + 1)] * (C / energy)**2) / (
                (C / energy)**2 - kwargs["L%d" % (i + 1)]**2)

        return sum(epsilon, 0) + 1
Пример #6
0
def critical_thickness(layer_material="GaInAs", lattice_material="GaAs", layer_fraction=0, lattice_fraction=None, T=300,
                       during_growth=True, final_unit="m", bowed_material="In"):
    science_reference("critical thickness",
                      "Matthews, J., & Blakeslee, A. (1974). Defects in epitaxial multilayers: I. Misfit dislocations. "
                      "Journal of Crystal Growth, 27, 118-125.")
    get_vurgaftman_parameter = ParameterSystem().get_parameter

    other_params = {
        "T": T,
        bowed_material: layer_fraction,
    }
    c11 = get_vurgaftman_parameter(layer_material, "c11", **other_params)
    c12 = get_vurgaftman_parameter(layer_material, "c12", **other_params)
    a = get_vurgaftman_parameter(layer_material, "lattice_constant", **other_params)
    a_lattice = get_vurgaftman_parameter(lattice_material, "lattice_constant", **other_params)

    b = array(a / sqrt(2))
    v = array(abs(c12 / (c11 + c12)))
    f = array(abs(a / a_lattice - 1))

    longest = max([each.size for each in [v, b, f]])

    if longest != 1:
        if b.size != longest:
            assert b.size == 1, "array arguments do not match length"
            b = ones(longest) * b
        if v.size != longest:
            assert v.size == 1, "array arguments do not match length"
            v = ones(longest) * v
        if f.size != longest:
            assert f.size == 1, "array arguments do not match length"
            f = ones(longest) * f
    else:
        v, b, f = [v], [b], [f]

    result = []
    for vi, fi, bi in zip(v, f, b):
        roots_at_soln = lambda hc_over_b: (hc_over_b) / (log(hc_over_b) + 1) - (1 - vi / 4) / (
            fi * pi * (1 + vi))  # matthews & blakeslee
        try:
            hc = bisect(roots_at_soln, 1e-10, 1e10) * bi
        except ValueError:
            raise ValueError("Critical thickness infinite?")

        result.append(convert(hc / 4, "Ang", final_unit)) if during_growth else result.append(
            convert(hc, "Ang", final_unit))
    return array(result)
Пример #7
0
def calculate_mobility(material, holes, N, x=0.0, y=0.0, T=300):
    """ Calculates the mobility using the model by Sotoodeh et al. If the material is not in the database, then the function returns the mobility for GaAs at that temperature, T, and impurity concentration, N.

    :param material: A string with the material name
    :param holes: If calculation should be done for electrons (holes=0) or holes (holes=1)
    :param N: Impurity concentration
    :param x: The fractional composition in the case of ternaries
    :param y: The other fractional composition in the case of quaternaries
    :param T: Temperature
    :return: The calculated mobility
    """
    science_reference('mobility calculator', 'M. Sotoodeh, A. H. Khalid, and A. A. Rezazadeh,'
                                             '“Empirical low-field mobility model for III–V compounds applicable in device simulation codes,"'
                                             'J. Appl. Phys., vol. 87, no. 6, p. 2890, 2000.')

    i = 1
    if holes: i = 2

    if material not in data.keys():
        print("Error: Material {0} not in the database for the mobility. Reverting to GaAs.".format(material))
        d = data['GaAs'][i]
    elif data[material][0] == 2:
        d = data[material][i]
    elif material == "InGaAs":
        d = calculate_InGaAs(x, i)
    elif material == "GaInP":
        d = calculate_InGaP(x, i, T)
    elif material == "AlGaAs":
        d = calculate_AlGaAs(x, i, T)
    elif material == "InAlAs":
        d = calculate_InAlAs(x, i, T)
    elif material == "InGaAsP":
        d = calculate_InGaAsP(x, y, i, T)
    else:
        d = calculate_General(material, x, i, T)

    muMin = d["muMin"]
    muMax = d["muMax"]
    Nref = d["Nref"]
    l = d["l"]
    t1 = d["t1"]
    t2 = d["t2"]

    m = mobility_low_field(N / 1e6, muMin, muMax, Nref, l, t1, t2, T) / 10000  # To convert it from cm2 to m2
    return m
Пример #8
0
def reference_spectra():
    """ Function providing the standard reference spectra: AM0, AM1.5g and AM1.5d.

    :return: A 2D array with 4 columns representing the wavelength, AM0, AM1.5g and AM1.5d standard spectra."""

    science_reference(
        "Standard solar spectra",
        "ASTM G173-03(2012), Standard Tables for Reference Solar Spectral Irradiances: "
        "Direct Normal and Hemispherical on 37° Tilted Surface, ASTM International, "
        "West Conshohocken, PA, 2012, www.astm.org")

    this_dir = os.path.split(__file__)[0]
    output = np.loadtxt(os.path.join(this_dir, "astmg173.csv"),
                        dtype=float,
                        delimiter=',',
                        skiprows=2)

    return output
Пример #9
0
def exciton_rydberg_energy_2d(me, mh, eps_r):
    """
    :param me: electron effective mass (units: kg)
    :param mh: hole effective mass (units: kg)
    :param eps_r: dielectic constant (units: SI)
    :return: The exciton Rydberg energy
    """

    science_reference("Definition of the exciton Rydberg Energy for a quantum well.",
                      "S. L. Chuang, Physics of Optoelectonic Devices, Second Edition, p.554, Table 13.1")

    mr = me * mh / (me + mh)  # reduced effective mass
    if True:
        return mr * q ** 4 / (2 * hbar ** 2 * (4 * np.pi * eps_r * vacuum_permittivity) ** 2)
    Ry = 13.6 * 1.6e-19
    m0 = electron_mass
    Ry_eff = (mr / m0) * Ry / eps_r ** 2  # Effective Rydberg
    return Ry_eff
Пример #10
0
    def Broad(self, Gamma, Alpha, E0, energy):
        """
        Custom_CPPB.Broad() :: defines the frequency dependent gaussian broadening function proposed by C. Kim.

        :param Gamma: Broadening parameter (eV).
        :param Alpha: Parameter describing the transition from pure Lorentzian (Alpha=0) to approximated Gaussian
                (Alpha = 0.2) lineshape broadneing.
        :param E0: Critical point centre energy (eV).
        :param energy: Energy array (eV).

        :return: Frequency dependent broadening parameter.
        """

        # Scientific reference for this work...
        science_reference(
            "Charles Kim, 'Modelling the optical dielectric function of semiconductors",
            "C. C. Kim et al, 'Modelling the optical dielectric function of semiconductors: Extension of "
            "the critical-point parabolic band approximation', Physical Review B 45(20) 11749, 1992"
        )

        return Gamma * np.exp(-1 * Alpha * ((energy - E0) / Gamma)**2)
Пример #11
0
def calculate_mobility(material, holes, N, x=0.0, y=0.0, T=300):
    science_reference(
        'mobility calculator',
        'M. Sotoodeh, A. H. Khalid, and A. A. Rezazadeh,'
        '“Empirical low-field mobility model for III–V compounds applicable in device simulation codes,"'
        'J. Appl. Phys., vol. 87, no. 6, p. 2890, 2000.')

    i = 1
    if holes: i = 2

    if material not in data.keys():
        print(
            "Error: Material {0} not in the database for the mobility. Reverting to GaAs."
            .format(material))
        d = data['GaAs'][i]
    elif data[material][0] == 2:
        d = data[material][i]
    elif material == "InGaAs":
        d = calculate_InGaAs(x, i)
    elif material == "GaInP":
        d = calculate_InGaP(x, i, T)
    elif material == "AlGaAs":
        d = calculate_AlGaAs(x, i, T)
    elif material == "InAlAs":
        d = calculate_InAlAs(x, i, T)
    elif material == "InGaAsP":
        d = calculate_InGaAsP(x, y, i, T)
    else:
        d = calculate_General(material, x, i, T)

    muMin = d["muMin"]
    muMax = d["muMax"]
    Nref = d["Nref"]
    l = d["l"]
    t1 = d["t1"]
    t2 = d["t2"]

    m = mobility_low_field(N / 1e6, muMin, muMax, Nref, l, t1, t2,
                           T) / 10000  # To convert it from cm2 to m2
    return m
Пример #12
0
def spectral_response_all_junctions(solar_cell,
                                    incident_light=None,
                                    energy=None,
                                    V=0,
                                    verbose=False):
    """ Calculates the spectral response of any number of junctions using analytical diode equations as described in
    J. Nelson's book "The Physics of Solar Cells" (2003). All parameters must be in SI units. It only works for
    homojunctions.

    :param solar_cell: A structure object with one or more Junction objects and a few optional parameters. Each junction
     is made of a sequence of *Layers* (with a thickness, a material and a role) and the surface recombination
     velocities for electrons and holes. Optional attributes for the structure are:
        - shading: (optional) Shading loses.
        - reflectivity: (optional) Function that calculates the reflectivity as a function of energy (in J).
    :param incident_light: (optional) 2D array containing the energies and the spectral power density (in photons m-2 J-1).
    :param energy: (optional) energies at which to calculate the QE. If not provided, the range of the incident ligth
    is used and if that is not defined, a "sensible" range is used.
    :param V: The voltage at which to perform the calculations (in V)
    :param verbose: If the information about the calculation must be printed (default = FALSE).
    :return: A dictionary containing, as a function of energy:

        - junctions: A list containing the QE data for each junction
        - transmitted: The transmitted power density
        - transmitted_fraction: The fraction of transmitted power
        - passive_loss: The passive losses (light absorbed in the encapsulants, AR coatings, window layers, etc)
        - reflected: reflected power density
        - reflected_fraction: Fraction of reflected power
        - e: The energy values
    """
    science_reference(
        "Nelson pin spectral response",
        "Jenny: (Nelson. The Physics of Solar Cells. Imperial College Press (2003))"
    )

    # Get the energy range and incident spectrum. If they are not inputs, we create some sensible values.
    if energy is None:
        if incident_light is not None:
            energy = incident_light[0]
            bs = np.copy(incident_light[1])
        else:
            energy = siUnits(np.linspace(0.5, 3.5, 450), 'eV')
            bs = np.ones_like(energy)
    else:
        if incident_light is not None:
            bs = np.interp(energy, incident_light[0], incident_light[1])
        else:
            bs = np.ones_like(energy)

    bs_initial = np.copy(bs)

    # We include the shadowing losses
    if hasattr(solar_cell, 'shading'):
        bs *= (1 - solar_cell.shading)

    # And the reflexion losses
    if hasattr(solar_cell,
               'reflectivity') and solar_cell.reflectivity is not None:
        ref = solar_cell.reflectivity(energy)
        bs *= (1 - ref)
        reflected = ref * bs_initial
    else:
        reflected = np.zeros_like(bs)

    # And now we perform the calculation, each junction at a time
    qe_result = []
    passive_loss = np.ones_like(bs)

    for layer_index, layer_object in enumerate(solar_cell):

        # Attenuation due to absorption in the AR coatings or any layer in the front that is not part of the
        # junction
        if type(layer_object) is Layer:
            bs = bs * np.exp(
                -layer_object.material.alphaE(energy) * layer_object.width)
            passive_loss *= np.exp(-layer_object.material.alphaE(energy) *
                                   layer_object.width)

        # For each junction, we calculate the spectral response
        elif type(layer_object) is Junction:

            # If there are window layers, passively absorbing light above the emitter, we attenuate the intensity
            idx = 0
            for junction_layer_object in layer_object:
                if junction_layer_object.role != 'emitter':
                    bs = bs * np.exp(
                        -junction_layer_object.material.alphaE(energy) *
                        junction_layer_object.width)
                    passive_loss *= np.exp(
                        -junction_layer_object.material.alphaE(energy) *
                        junction_layer_object.width)
                    idx += 1
                else:
                    break

            output = calculate_junction_sr(layer_object,
                                           energy,
                                           bs,
                                           bs_initial,
                                           V,
                                           printParameters=verbose)
            qe_result.append(output)

            # And we reduce the amount of light reaching the next junction
            for junction_layer_object in layer_object[idx:]:
                bs *= np.exp(-junction_layer_object.material.alphaE(energy) *
                             junction_layer_object.width)

        else:
            raise ValueError(
                "Strange layer-like object discovered in structure stack: {}".
                format(type(layer_object)))

    return {
        "junctions": qe_result,
        "transmitted": bs,
        "transmitted_fraction": bs / bs_initial,
        "passive_loss": 1 - passive_loss,
        "reflected": reflected,
        "reflected_fraction": reflected / bs_initial,
        "e": energy
    }
Пример #13
0
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()
    }
Пример #14
0
    def E2(self, energy, material_parameters, **kwargs):
        """
        Custom_CPPB.E2() :: S. Adachi finds that the high energy E2 critical point is well approximated using a simple
            damped harmonic oscillator.

        :param energy: energy: Energy array (eV).
        :param material_parameters: Parameter set imported using Material_Parameters() method. Not required as long as
            keyword arguments are specified.
        :param kwargs: These take in individual parameters for the model. Keywords should take the following form;
                    Gamma_E2
                    Alpha_E2
                    E2
                    C

        :return: E2 contributions to the complex dielectric function.
        """

        # Scientific reference for this work...
        science_reference(
            "Sadao Adachi, Physical Properties of III-V Semiconductor Compounds",
            "Adachi, S., Physical Properties of III-V Semiconductor Compounds, John Wiley & Sons (1992)"
        )

        # Conditional statement determining where the input parameters come from...
        if material_parameters is None and bool(kwargs) is False:

            raise ValueError("No material parameters specified...")

        elif material_parameters is not None and bool(kwargs) is False:

            Params = material_parameters

        elif material_parameters is not None and bool(kwargs) is True:

            Params = material_parameters

            for key in kwargs:

                try:

                    Params[key] = kwargs[key]
                except KeyError:
                    print("Invalid material parameter...")

        elif bool(kwargs) is True:

            Params = {}

            for key in kwargs:

                try:

                    Params[key] = kwargs[key]
                except KeyError:
                    print("Invalid material parameter...")

        # Frequency dependent broadning parameter...
        Gamma = self.Broad(Params["Gamma_E2"], Params["Alpha_E2"],
                           Params["E2"], energy)

        # Damped harmonic oscillator function described by Adachi...
        Eps = Params["C"] / ((1 - (energy / Params["E2"])**2) - 1j *
                             (energy / Params["E2"]) * Gamma)

        # Additional line to address change in phase of the imaginary signal.
        return Eps.real + 1j * abs(Eps.imag)
Пример #15
0
def tridiag_euler(V,
                  z,
                  m,
                  periodic=False,
                  num_eigenvalues=10,
                  quasiconfined=0):
    """
    Returns eignvalue and eigenvectors of an arbitrary potential.
    
    A tridiagonal matrix is constructed by writing the variable effective
    mass Schrodinger equation over a series of mesh points. The eigenvalues
    of the matrix correspond to the allowed energy levels of the system.
    
    The previous solver, eig, has been replaced by the spare matrix version, eigs, that is faster to compute
    """

    science_reference(
        "Varible effective mass Schordinger equation and tridiagonal solution method.",
        "Frensley, W. R. (1991). \
                      Numerical evaluation of resonant states. \
                      Superlattices and Microstructures, 11(3), 347350. \
                      doi:10.1016/0749-6036(92)90396-M")

    N = len(V)
    dz = np.gradient(z)
    m = m * ones(N)

    # Vectorise effective mass differences to avoid a loop
    m = np.insert(m, (0, len(m)), (m[0], m[-1]))
    m_a = m[0:-2]  # m_(j-1) from Frensley, W. R. (1991)
    m_b = m[1:-1]  # m_j
    m_c = m[2:]  # m_(j+1)

    # These are the interior diagonals of equation 18 in the above ref.
    axis = hbar**2 / (4 * dz**2) * (
        1 / m_a + 2 / m_b + 1 / m_c) + V  # d_j from Frensley, W. R. (1991)
    upper = hbar**2 / (4 * dz**2) * (1 / m_a + 1 / m_b)  # s_(j+1)
    lower = hbar**2 / (4 * dz**2) * (1 / m_b + 1 / m_c)  # s_j

    if periodic:
        TopRight = np.zeros(len(axis))
        BottomLeft = np.zeros(len(axis))
        TopRight[-1] = -lower[
            -1]  # The last point becomes the one before the first one
        BottomLeft[0] = -upper[
            0]  # The first point becomes the one after the last one

        index = (-N + 1, -1, 0, 1, N - 1)
        diagonals = (BottomLeft, -lower, axis, -upper, TopRight)

    else:
        index = (-1, 0, 1)
        diagonals = (-lower, axis, -upper)

    H = dia_matrix((diagonals, index), shape=(N, N))

    # H[0,-1] = -lower[-1]    # The last point becomes the one before the first one
    # H[-1,0] = -upper[0]     # The first point becomes the one after the last one

    sigma = np.min(
        V
    )  # The top of the potential (valence band) is the target energy for the eigenvalues

    # The heavy numerical calculation
    E, Psi = eigs(H, k=num_eigenvalues, which='LR', sigma=sigma)

    # Allow for quasi confined levels to go through. They can be discarded later with the filter
    confined_levels = [i for i, e in enumerate(E) if e < quasiconfined * q]
    E, Psi = E[confined_levels].real, array(Psi[:,
                                                confined_levels]).transpose()
    Psi = [(p / sqrt(trapz(p * p, x=z))).real for p in Psi]

    E, Psi = sort_simultaneous(E, Psi)

    return E, Psi
def get_J_sc_diffusion_green(xa, xb, g, D, L, y0, S, ph, side='top'):
    """Computes the derivative of the minority carrier concentration at the edge of the junction by approximating the convolution integral resulting from applying the Green's function method to the drift-diffusion equation.

    :param xa: Coordinate at the start the junction.
    :param xb: Coordinate at the end the junction.
    :param g: Carrier generation rate at point x (expected as function).
    :param D: Diffusion constant.
    :param L: Diffusion length.
    :param y0: Carrier equilibrium density.
    :param S: Surface recombination velocity.
    :param ph: Light spectrum.
    :param side: String to indicate the edge of interest. Either 'top' or 'bottom'.

    :return: The derivative of the minority carrier concentration at the edge of the junction.
    """

    science_reference(
        'DA Green\'s function method.',
        'T. Vasileiou, J. M. Llorens, J. Buencuerpo, J. M. Ripalda, D. Izzo and L. Summerer, “Light absorption enhancement and radiation hardening for triple junction solar cell through bioinspired nanostructures,” Bioinspir. Biomim., vol. 16, no. 5, pp. 056010, 2021.'
    )

    xbL = (xb - xa) / L
    crvel = S / D * L
    ph_over_D = ph / D

    # if L too low in comparison to junction width, avoid nan's
    if xbL > 1.e2:
        if side == 'top':
            cadd = -y0 / L
            fun = partial(_conv_exp_top,
                          xa=xa,
                          xb=xb,
                          g=g,
                          L=L,
                          phoD=ph_over_D)
        else:
            cadd = y0 / L
            fun = partial(_conv_exp_bottom, xa=xa, g=g, L=L, phoD=ph_over_D)
        cp = 1.
    else:
        if side == 'top':
            cp = -np.cosh(xbL) - crvel * np.sinh(xbL)
            cadd = (np.sinh(xbL) + crvel * np.cosh(xbL)) * y0 / L
            fun = partial(_conv_green_top,
                          xa=xa,
                          xb=xb,
                          g=g,
                          L=L,
                          phoD=ph_over_D,
                          crvel=crvel)
        else:
            cp = np.cosh(xbL) - crvel * np.sinh(xbL)
            cadd = (np.sinh(xbL) - crvel * np.cosh(xbL)) * y0 / L
            fun = partial(_conv_green_bottom,
                          xb=xb,
                          g=g,
                          L=L,
                          phoD=ph_over_D,
                          crvel=crvel)

    out, err = quad_vec(fun, xa, xb, epsrel=1.e-5)
    return (out.squeeze() + cadd) / cp
def iv_depletion(junction, options):
    """ Calculates the IV curve of a junction object using the depletion approximation as described in J. Nelson, “The Physics of Solar Cells”, Imperial College Press (2003). The junction is then updated with an "iv" function that calculates the IV curve at any voltage.

    :param junction: A junction object.
    :param options: Solver options.
    :return: None.
    """

    science_reference(
        'Depletion approximation',
        'J. Nelson, “The Physics of Solar Cells”, Imperial College Press (2003).'
    )

    junction.voltage = options.internal_voltages
    T = options.T
    kbT = kb * T

    id_top, id_bottom, pRegion, nRegion, iRegion, pn_or_np = identify_layers(
        junction)
    xn, xp, xi, sn, sp, ln, lp, dn, dp, Nd, Na, ni, es = identify_parameters(
        junction, T, pRegion, nRegion, iRegion)

    niSquared = ni**2

    Vbi = (kbT / q) * np.log(Nd * Na / niSquared) if not hasattr(
        junction, "Vbi") else junction.Vbi  # Jenny p146

    #Na, Nd, ni, niSquared, xi, ln, lp, xn, xp, sn, sp, dn, dp, es, id_top, id_bottom, Vbi, pn_or_np = process_junction(junction, options)

    R_shunt = min(junction.R_shunt, 1e14) if hasattr(junction,
                                                     'R_shunt') else 1e14

    # And now we account for the possible applied voltage, which can be, at most, equal to Vbi
    V = np.where(junction.voltage < Vbi - 0.001, junction.voltage, Vbi - 0.001)

    wn, wp = get_depletion_widths(junction, es, Vbi, V, Na, Nd, xi)

    w = wn + wp + xi

    # Now it is time to calculate currents
    if pn_or_np == "pn":
        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:
        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

    JtopDark = get_j_dark(x_top, w_top, l_top, s_top, d_top, V, min_top, T)
    JbotDark = get_j_dark(x_bottom, w_bottom, l_bottom, s_bottom, d_bottom, V,
                          min_bot, T)

    # hereby we define the subscripts to refer to the layer in which the current is generated:
    if pn_or_np == "pn":
        JnDark, JpDark = JbotDark, JtopDark
    else:
        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

    # Here we use the full version of the SRH recombination term as calculated by Sah et al. Works for positive bias
    # and moderately negative ones.

    science_reference(
        'SRH current term.',
        'C. T. Sah, R. N. Noyce, and W. Shockley, “Carrier Generation and Recombination in P-N Junctions and P-N Junction Characteristics,” presented at the Proceedings of the IRE, 1957, vol. 45, no. 9, pp. 1228–1243.'
    )
    Jrec = get_Jsrh(ni, V, Vbi, lifetime_p, lifetime_n, w, kbT)

    J_sc_top = 0
    J_sc_bot = 0
    J_sc_scr = 0

    if options.light_iv:

        widths = []
        for layer in junction:
            widths.append(layer.width)

        cum_widths = np.cumsum([0] + widths)

        g = junction.absorbed
        wl = options.wavelength
        wl_sp, ph = options.light_source.spectrum(
            output_units='photon_flux_per_m', x=wl)
        id_v0 = np.argmin(abs(V))

        # The contribution from the Emitter (top side).
        xa = cum_widths[id_top]
        xb = cum_widths[id_top + 1] - w_top[id_v0]

        if options.da_mode == 'bvp':
            deriv = get_J_sc_diffusion(xa,
                                       xb,
                                       g,
                                       d_top,
                                       l_top,
                                       min_top,
                                       s_top,
                                       wl,
                                       ph,
                                       side='top')
        else:
            xbb = xb - (xb - xa) / 1001.
            deriv = get_J_sc_diffusion_green(xa,
                                             xbb,
                                             g,
                                             d_top,
                                             l_top,
                                             min_top,
                                             s_top,
                                             ph,
                                             side='top')
            deriv = np.trapz(deriv, wl)
        J_sc_top = q * d_top * abs(deriv)

        # The contribution from the Base (bottom side).
        xa = cum_widths[id_bottom] + w_bottom[id_v0]
        xb = cum_widths[id_bottom + 1]
        if options.da_mode == 'bvp':
            deriv = get_J_sc_diffusion(xa,
                                       xb,
                                       g,
                                       d_bottom,
                                       l_bottom,
                                       min_bot,
                                       s_bottom,
                                       wl,
                                       ph,
                                       side='bottom')
        else:
            xbb = xb - (xb - xa) / 1001.
            deriv = get_J_sc_diffusion_green(xa,
                                             xbb,
                                             g,
                                             d_bottom,
                                             l_bottom,
                                             min_bot,
                                             s_bottom,
                                             ph,
                                             side='bottom')
            deriv = np.trapz(deriv, wl)
        J_sc_bot = q * d_bottom * abs(deriv)

        # The contribution from the SCR (includes the intrinsic region, if present).
        xa = cum_widths[id_top + 1] - w_top[id_v0]
        xb = cum_widths[id_bottom] + w_bottom[id_v0]
        J_sc_scr = q * get_J_sc_SCR(xa, xb, g, wl, ph)

    # And, finally, we output the currents
    junction.current = Jrec + JnDark + JpDark + V / R_shunt - J_sc_top - J_sc_bot - J_sc_scr
    junction.iv = interp1d(junction.voltage,
                           junction.current,
                           kind='linear',
                           bounds_error=False,
                           assume_sorted=True,
                           fill_value=(junction.current[0],
                                       junction.current[-1]))
    junction.region_currents = State({
        "Jn_dif": JnDark,
        "Jp_dif": JpDark,
        "Jscr_srh": Jrec,
        "J_sc_top": J_sc_top,
        "J_sc_bot": J_sc_bot,
        "J_sc_scr": J_sc_scr
    })
Пример #18
0
    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:

        - Barriers have the bulk absorption
        - Interlayers have the bulk absorption from the barrier energy and zero below that
        - 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."""

        science_reference(
            "Absorption for QWs in the PDD solver",
            "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
Пример #19
0
    def RecalculateDensityOfStates(self, SR):
        """ Calculates the effective density of states for each layer in the QW. The general rule is:

        - Barriers have the bulk density of states
        - QW have ALL the density of states asociated with the confined states + bulk density of states above the barrier
        - 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"""

        science_reference(
            "Density of state for QWs in the PDD solver",
            "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"
        )

        # 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)

        e_prob = []
        hh_prob = []
        lh_prob = []
        for i in range(len(self.elevels)):
            e_prob.append(
                self.get_probability_per_layer(
                    SR["x"], SR["wavefunctions"]['psi_e'][i]**2))
        for i in range(len(self.hhlevels)):
            hh_prob.append(
                self.get_probability_per_layer(
                    SR["x"], SR["wavefunctions"]['psi_hh'][i]**2))
        for i in range(len(self.lhlevels)):
            lh_prob.append(
                self.get_probability_per_layer(
                    SR["x"], SR["wavefunctions"]['psi_lh'][i]**2))

        for index in range(len(self)):
            Ncc = self[index].material.Ncc
            Nvhh = self[index].material.Nvhh
            Nvlh = self[index].material.Nvlh

            #   1- Contribution from bulk:
            Vconf = self[index].electron_affinity - Xb
            self[index].material.__dict__["Nc"] = Ncc * erfc(
                max(0, b * Vconf)**0.5)

            #   2- Contribution from confined levels
            Nqw = 0
            for i, eLevel in enumerate(self.elevels):
                Nqw = Nqw + np.exp(-b * eLevel) * e_prob[i][index]

            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(
                max(0, b * Vconf)**0.5)

            #   2- Contribution from heavy holes confined levels
            Nqw = 0
            for i, hhLevel in enumerate(self.hhlevels):
                Nqw = Nqw + (Nvhh / 2.)**(2. / 3.) * np.exp(
                    -b * hhLevel) * hh_prob[i][index]

                #   3- Contribution from light holes confined levels
            for i, lhLevel in enumerate(self.lhlevels):
                Nqw = Nqw + (Nvlh / 2.)**(2. / 3.) * np.exp(
                    -b * lhLevel) * lh_prob[i][index]

            Nqw = 2. / self[index].width * Nqw
            self[index].material.Nv = self[index].material.Nv + Nqw
Пример #20
0
    def E0_Exciton(self, energy, material_parameters, **kwargs):
        """
        Custom_CPPB.E0_Exciton() :: From Adachi's formalism, contributions to the complex dielectric function from bound
            excitons in the vicinity of the E0 fundamental band-gap.

        :param energy: energy: Energy array (eV).
        :param material_parameters: Parameter set imported using Material_Parameters() method. Not required as long as
            keyword arguments are specified.
        :param kwargs: These take in individual parameters for the model. Keywords should take the following form;
                    Gamma_Ex0
                    Alpha_Ex0
                    A_Ex
                    G_3D
                    n

        :return: Excitonic contributions at E0 to the complex dielectric function.
        """

        # Scientific reference for this work...
        science_reference(
            "Sadao Adachi, Physical Properties of III-V Semiconductor Compounds",
            "Adachi, S., Physical Properties of III-V Semiconductor Compounds, John Wiley & Sons (1992)"
        )

        # Conditional statement determining where the input parameters come from...
        if material_parameters is None and bool(kwargs) is False:

            raise ValueError("No material parameters specified...")

        elif material_parameters is not None and bool(kwargs) is False:

            Params = material_parameters

        elif material_parameters is not None and bool(kwargs) is True:

            Params = material_parameters

            for key in kwargs:

                try:

                    Params[key] = kwargs[key]
                except KeyError:
                    print("Invalid material parameter...")

        elif bool(kwargs) is True:

            Params = {}

            for key in kwargs:

                try:

                    Params[key] = kwargs[key]
                except KeyError:
                    print("Invalid material parameter...")

        # Frequency dependent broadening parameter...
        Gamma = self.Broad(Params["Gamma_Ex0"], Params["Alpha_Ex0"],
                           Params["E0"], energy)
        Eps_n = np.zeros((len(energy), int(Params["n"] + 1)), dtype="complex")

        for j in range(1, int(Params["n"] + 1)):
            Eps_n[:, j] = (Params["A_Ex"] / j ** 3) * ((Params["E0"] - (Params["G_3D"] / \
                                                                        (j ** 2)) - energy - 1j * Gamma) ** -1)

        Eps = np.sum(Eps_n, 1)

        # Additional line to address change in phase of the imaginary signal.
        return Eps.real + 1j * abs(Eps.imag)
Пример #21
0
def create_adachi_alpha(material, Esteps=(1.42, 6, 3000), T=300, wl=None):
    """ Calculates the n, k and absorption coefficient of a material using Adachi's formalism of critical points.

    :param material: A solcore material
    :param Esteps: (1.42, 6, 3000) A tuple with the start, end and step energies in which calculating the optical data
    :param T: (300) Temeprature in kelvin
    :param wl: (None) Optional array indicating the wavelengths in which calculating the data
    :return: A tuple containing 4 arrays: (Energy, n, k, alpha)
    """

    science_reference(
        "Adachi optical dispersion relations",
        'S.Adachi, “Optical dispersion relations for GaP, GaAs, GaSb, InP, InAs, InSb, AlxGa1−xAs, '
        'and In1−xGaxAsyP1−y” J.Appl.Phys., vol.66, no.12, pp.6030–12, 1989.')

    if type(material) != str:
        material = material.plain_string()

    e0 = get_parameter(material, "E0", T=T) + 0j
    Delta0 = get_parameter(material, "E0plusD0", T=T) + 0j - e0
    e1 = get_parameter(material, "E1", T=T) + 0j
    Delta1 = get_parameter(material, "E1plusD1", T=T) + 0j - e1
    e2 = get_parameter(material, "E2", T=T) + 0j
    egid = get_parameter(material, "Eg1d", T=T) + 0j
    a = get_parameter(material, "A", T=T) + 0j
    b1 = get_parameter(material, "B1", T=T) + 0j
    b11 = get_parameter(material, "B11", T=T) + 0j
    Gamma = get_parameter(material, "Gamma", T=T) + 0j  # , "eV",T=T)
    cc = get_parameter(material, "C", T=T) + 0j
    gamma = get_parameter(material, "gamma", T=T) + 0j
    d = get_parameter(material, "D", T=T) + 0j
    omegaphonon = 0
    b2 = b1
    b21 = b11

    a0 = get_parameter(material, "lattice_constant", T=T)

    b2 = 44 * (e1 + 2 * Delta1 / 3.) / (a0 * (e1 + Delta1)**2)
    b1 = 44 * (e1 + Delta1 / 3) / (a0 * e1**2)
    b2 = 0
    b1 = 7
    b11 = 2 * b1
    b21 = 2 * b2

    other_params = {
        "$e_0$": e0.real + 0.005,
        "$e_1$": e1.real,
        "$e_1+\Delta_1$": (e1 + Delta1).real,
        "$e_2$": e2.real,
        "$e_0+\Delta_0$": (e0 + Delta0).real,
        "$e_{g,1d}$": egid.real
    }
    # print b1, b2, b11,b21

    # print other_params
    if wl is not None:
        E = 1240 * 1e-9 / wl[::-1]
    else:
        E = np.linspace(*Esteps)  # electron Volts

    omega = E * 1.6e-19 / hbar

    re = lambda x: x.real
    H = lambda x: (x.real >= 0)
    i = 1j

    XiSO = E / (e0 + Delta0)
    Xi0 = E / e0

    f_Xi_0 = (Xi0)**-2 * (2 - (1 + Xi0)**0.5 - (1 - Xi0)**0.5 * H(1 - Xi0))
    f_Xi_s0 = (XiSO)**-2 * (2 - (1 + XiSO)**0.5 -
                            (1 - XiSO)**0.5 * H(1 - XiSO))  #

    # equation 7
    epsilon_2_e0 = (a / E**2) * (
        (E - e0)**0.5 * H((Xi0) - 1) + 0.5 *
        (E - e0 - Delta0)**0.5 * H((E / (e0 + Delta0)) - 1))  #
    epsilon_1_e0 = a * e0**-1.5 * (f_Xi_0 + 0.5 *
                                   (e0 / (e0 + Delta0))**1.5 * f_Xi_s0)

    Xi1 = E / e1  #
    Xi2 = E / e2  #
    Xi1s = E / (e1 + Delta1)  #

    # equation 12
    epsilon_2_e1 = pi * Xi1**-2 * (b1 - b11 * (e1 - E)**0.5 * H(1 - Xi1)) * H(
        re(b1 - b11 * (e1 - E)**0.5 * H(1 - Xi1)))  #
    epsilon_2_e1_d1 = pi * Xi1s**-2 * (
        b2 - b21 * (e1 + Delta1 - E)**0.5 * H(1 - Xi1s)
    )  # where is this from? #*H(b2-b21*(e1+Delta1-E)**0.5*H(1-Xi1s)) #
    # from numpy import sin
    #    epsilon_2_3dcp = pi*b1*Xi1**-2*H(Xi1-1) + pi*b2*Xi1s**-2*H(Xi1s-1) # +10*sin(30*E)#
    # epsilon_1_3dcp = -b1*(Xi1+i*Gamma/e1)**-2*log(1-(Xi1+i*Gamma/e1)**2) # Ned Version
    # epsilon_1_3dcp = -b1*Xi1**-2*log(1-Xi1**2)-b2*Xi1s**-2*log(1-Xi1s**2) #adachi version, no damping
    Gamma = 0.06
    E_damped = E + i * Gamma
    Xi1damped = E_damped / e1
    Xi1sdamped = E_damped / (e1 + Delta1)

    # equation 16
    epsilon_1_3dcp = -b1 * Xi1damped**-2 * np.log(
        1 - Xi1damped**2) - b2 * Xi1sdamped**-2 * np.log(
            1 - Xi1sdamped**2)  # adachi version
    epsilon_2_3dcp = pi * b1 * Xi1damped**-2 * H(
        Xi1 - 1) + pi * b2 * Xi1sdamped**-2 * H(Xi1s - 1)  # +10*sin(30*E)#

    # equation 16
    epsilon_2_e2 = cc * Xi2 * gamma / ((1 - Xi2**2)**2 + (Xi2 * gamma)**2) * H(
        E -
        e1)  # added last term for compatibility with adachi's graph, dammit.
    # # print log(epsilon_2_e2)
    epsilon_1_e2 = cc * (1 - Xi2**2) / ((1 - Xi2**2)**2 + (Xi2 * gamma)**2)  #
    # print egid
    XiG_plus = (egid + hbar * omegaphonon) / E
    XiG_minus = (egid - hbar * omegaphonon) / E
    XiCh = E / e1

    epsilon_2_indirect = d / E**2 * (
        E - egid + hbar * omegaphonon)**2 * H(1 - XiG_plus) * H(1 - XiCh)  #
    epsilon_2_indirect_b = d / E**2 * (
        E - egid - hbar * omegaphonon)**2 * H(1 - XiG_minus) * H(1 - XiCh)  #
    # epsilon_2_indirect_minus = d/E**2 *(E-egid - hbar*omegaphonon)**2 * H(1-XiG_plus) * H(1-XiCh)# markus made this up a bit
    # epsilon_2_indirect_b_minus = d/E**2 *(E-egid + hbar*omegaphonon)**2 * H(1-XiG_minus) * H(1-XiCh)#markus made this up a bit

    epsilon_2_indirect_total = epsilon_2_indirect + epsilon_2_indirect_b

    eps1 = epsilon_1_e0 + epsilon_1_3dcp + epsilon_1_e2
    eps2 = epsilon_2_e0 + epsilon_2_e1 + epsilon_2_e1_d1 + epsilon_2_e2 + epsilon_2_indirect_total

    eps2 = epsilon_2_e0 + epsilon_2_e1 + epsilon_2_e2 + epsilon_2_indirect_total

    E = E * 1.6e-19
    k = np.sqrt((np.sqrt(eps1**2 + eps2**2) - eps1) / 2).real
    n = np.sqrt((np.sqrt(eps1**2 + eps2**2) + eps1) / 2).real
    alpha_data = 2 * omega / c * k

    # If the input was in wavelengths, we reverse the output arrays
    if wl is not None:
        alpha_data = alpha_data[::-1]
        n = n[::-1]
        k = k[::-1]

    return E.real, abs(n + 1e-10), abs(k + 1e-10), abs(alpha_data + 1e-10)
def qe_depletion(junction, options):
    """ Calculates the QE curve of a junction object using the depletion approximation as described in J. Nelson, “The Physics of Solar Cells”, Imperial College Press (2003). The junction is then updated with an "iqe" and several "eqe" functions that calculates the QE curve at any wavelength.

    :param junction: A junction object.
    :param options: Solver options.
    :return: None.
    """

    science_reference(
        'Depletion approximation',
        'J. Nelson, “The Physics of Solar Cells”, Imperial College Press (2003).'
    )

    # First we have to figure out if we are talking about a PN, NP, PIN or NIP junction
    T = options.T
    kbT = kb * T

    id_top, id_bottom, pRegion, nRegion, iRegion, pn_or_np = identify_layers(
        junction)
    xn, xp, xi, sn, sp, ln, lp, dn, dp, Nd, Na, ni, es = identify_parameters(
        junction, T, pRegion, nRegion, iRegion)

    niSquared = ni**2

    Vbi = (kbT / q) * np.log(Nd * Na / niSquared) if not hasattr(
        junction, "Vbi") else junction.Vbi  # Jenny p146

    wn, wp = get_depletion_widths(junction, es, Vbi, 0, Na, Nd, xi)

    # Now it is time to calculate currents
    if pn_or_np == "pn":
        l_top, l_bottom = ln, lp
        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:
        l_bottom, l_top = ln, lp
        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

    widths = []
    for layer in junction:
        widths.append(layer.width)

    cum_widths = np.cumsum([0] + widths)

    g = junction.absorbed
    wl = options.wavelength
    wl_sp, ph = LightSource(source_type='black body', x=wl,
                            T=6000).spectrum(output_units='photon_flux_per_m',
                                             x=wl)

    # The contribution from the Emitter (top side).
    xa = cum_widths[id_top]
    xb = cum_widths[id_top + 1] - w_top

    if options.da_mode == 'bvp':
        deriv = get_J_sc_diffusion_vs_WL(xa,
                                         xb,
                                         g,
                                         d_top,
                                         l_top,
                                         min_top,
                                         s_top,
                                         wl,
                                         ph,
                                         side='top')
    else:
        xbb = xb - (xb - xa) / 1001.
        deriv = get_J_sc_diffusion_green(xa,
                                         xbb,
                                         g,
                                         d_top,
                                         l_top,
                                         min_top,
                                         s_top,
                                         ph,
                                         side='top')
    j_sc_top = d_top * abs(deriv)

    # The contribution from the Base (bottom side).
    xa = cum_widths[id_bottom] + w_bottom
    xb = cum_widths[id_bottom + 1]

    if options.da_mode == 'bvp':
        deriv = get_J_sc_diffusion_vs_WL(xa,
                                         xb,
                                         g,
                                         d_bottom,
                                         l_bottom,
                                         min_bot,
                                         s_bottom,
                                         wl,
                                         ph,
                                         side='bottom')
    else:
        xbb = xb - (xb - xa) / 1001.
        deriv = get_J_sc_diffusion_green(xa,
                                         xbb,
                                         g,
                                         d_bottom,
                                         l_bottom,
                                         min_bot,
                                         s_bottom,
                                         ph,
                                         side='bottom')
    j_sc_bot = d_bottom * abs(deriv)

    # The contribution from the SCR (includes the intrinsic region, if present).
    xa = cum_widths[id_top + 1] - w_top
    xb = cum_widths[id_bottom] + w_bottom
    j_sc_scr = get_J_sc_SCR_vs_WL(xa, xb, g, wl, ph)

    # The total light absorbed, but not necessarily collected, is:
    xa = cum_widths[id_top]
    xb = cum_widths[id_bottom + 1]
    zz = np.linspace(xa, xb, 10001)
    gg = g(zz) * ph
    current_absorbed = np.trapz(gg, zz, axis=0)

    # Now, we put everything together
    j_sc = j_sc_top + j_sc_bot + j_sc_scr

    eqe = j_sc / ph
    eqe_emitter = j_sc_top / ph
    eqe_base = j_sc_bot / ph
    eqe_scr = j_sc_scr / ph

    iqe = j_sc / current_absorbed
    iqe[np.isnan(
        iqe
    )] = 0  # if zero current_absorbed, get NaN in previous line; want 0 IQE

    junction.iqe = interp1d(wl, iqe)

    junction.eqe = interp1d(wl,
                            eqe,
                            kind='linear',
                            bounds_error=False,
                            assume_sorted=True,
                            fill_value=(eqe[0], eqe[-1]))
    junction.eqe_emitter = interp1d(wl,
                                    eqe_emitter,
                                    kind='linear',
                                    bounds_error=False,
                                    assume_sorted=True,
                                    fill_value=(eqe_emitter[0],
                                                eqe_emitter[-1]))
    junction.eqe_base = interp1d(wl,
                                 eqe_base,
                                 kind='linear',
                                 bounds_error=False,
                                 assume_sorted=True,
                                 fill_value=(eqe_base[0], eqe_base[-1]))
    junction.eqe_scr = interp1d(wl,
                                eqe_scr,
                                kind='linear',
                                bounds_error=False,
                                assume_sorted=True,
                                fill_value=(eqe_scr[0], eqe_scr[-1]))

    junction.qe = State({
        'WL': wl,
        'IQE': junction.iqe(wl),
        'EQE': junction.eqe(wl),
        'EQE_emitter': junction.eqe_emitter(wl),
        'EQE_base': junction.eqe_base(wl),
        'EQE_scr': junction.eqe_scr(wl)
    })
Пример #23
0
class sopra_database:
    """Import the SOPRA_DB module from the solcore.material_system package and get started by selecting a material from
    the extensive list that SOPRA-SA compiled;

    >>> GaAs = sopra_database('GaAs')

    Once imported a number of useful methods can be called to return n, k and alpha data for the desired material.
    """

    science_reference("All optical constant data made avaialble by SOPRA-SA",
                      "http://www.sspectra.com/sopra.html")

    def __init__(self, Material):
        # Define filepath to the SOPRA database for file import...
        self.__SOPRA_PATH = SOPRA_PATH

        # Load in SOPRA_DB.csv database file
        DB = np.genfromtxt(os.path.join(self.__SOPRA_PATH,
                                        "SOPRA_DB_Updated.csv"),
                           delimiter=",",
                           dtype=str)

        self.__fname = None
        for fname, symbol, range, info in DB:

            if re.fullmatch(Material.upper(), fname) is not None:
                self.__fname = fname

                self.path = os.path.join(self.__SOPRA_PATH,
                                         self.__fname + ".MAT")

                # self.info contains all detail loaded from SOPRA_DB,csv file...
                self.info = {
                    "Material": symbol,
                    "Wavelength (nm)": range,
                    "File Info": info,
                    "File Path": self.path
                }

        # If the material name is incorrect then the material attribute is not written to
        if self.__fname is None:

            print(
                "SOPRA_DB :: ERROR :: Material not found in SOPRA_DB... Check materials list..."
            )
            print("Similar Matches ::")
            for fname, symbol, range, info in DB:

                if re.match(Material.upper(), fname) is not None:
                    print(fname)

            # If the exception is caught, exit the program as nothing else useful can be done...
            # sys.exit()
            raise SOPRAError(
                "Material not found in SOPRA database: {}".format(Material))

    @staticmethod
    def material_list():
        """ SOPRA_DB.material_list() :: Loads a list (.pdf file) of all available SOPRA materials. """

        print("Opening List of Available Materials in the SOPRA database")

        # Need different treatment depending on computer OS.
        if sys.platform == 'darwin':
            # Find spaces in the filename and add a \ before (for unix based systems)
            directory = SOPRA_PATH.split(" ")

            new_path = directory[0]
            for i in range(1, len(directory), 1):
                new_path = new_path + "\ " + directory[i]

            os.system("open " +
                      os.path.join(new_path, "List_Of_Files_Updated_PDF.pdf"))

        elif sys.platform == 'linux':
            # Find spaces in the filename and add a \ before (for unix based systems)
            directory = SOPRA_PATH.split(" ")

            new_path = directory[0]
            for i in range(1, len(directory), 1):
                new_path = new_path + "\ " + directory[i]

            os.system("xdg-open " +
                      os.path.join(new_path, "List_Of_Files_Updated_PDF.pdf"))

        elif sys.platform == 'win32':
            # Find spaces in the filename and add a \ before (for unix based systems)

            os.system(
                "start " +
                os.path.join(SOPRA_PATH, "List_Of_Files_Updated_PDF.pdf"))

    def load_n(self, Lambda=None):
        """ SOPRA_DB.load_n(Lambda) :: Load refractive index (n) data of the requested material.
            Optional argument Lambda allows user to specify a custom wavelength range. data will be interpolated into
                this range before output.

            Returns: Tuple of (Wavelength, n)"""

        try:
            os.stat(self.path)
        except FileNotFoundError:
            print(
                'load_n :: WARNING :: There is no individual data file for, ' +
                self.material + ".")
            print(
                'This material may be part of a set of varying composition, check the materials list...'
            )
            sys.exit()

        # Load in data from file...
        Wav, n = np.genfromtxt(self.path,
                               delimiter="*",
                               skip_header=3,
                               skip_footer=3,
                               usecols=(2, 3),
                               unpack=True)

        if Lambda is not None:
            # Interpolate in range specified by Lambda...
            n_interp = np.interp(Lambda, Wav, n)

            return (Lambda, n_interp)

        else:
            return (Wav, n)

    def load_k(self, Lambda=None):
        """ SOPRA_DB.load_k(Lambda) :: Load refractive index (n) data of the requested material.
            Optional argument Lambda allows user to specify a custom wavelength range. data will be interpolated into
                this range before output.

            Returns: Tuple of (Wavelength, k) """

        try:
            os.stat(self.path)
        except FileNotFoundError:
            print(
                'load_k :: WARNING :: There is no individual data file for, ' +
                self.material + ".")
            print(
                'This material may be part of a set of varying composition, check the materials list...'
            )
            sys.exit()

        # Load in data from file...
        Wav, k = np.genfromtxt(self.path,
                               delimiter="*",
                               skip_header=3,
                               skip_footer=3,
                               usecols=(2, 4),
                               unpack=True)

        if Lambda is not None:
            # Interpolate in range specified by Lambda...
            k_interp = np.interp(Lambda, Wav, k)

            return (Lambda, k_interp)
        else:
            return (Wav, k)

    def load_alpha(self, Lambda=None):
        """ SOPRA_DB.load_alpha(Lambda) :: Load refractive index (n) data of the requested material.
            Optional argument Lambda allows user to specify a custom wavelength range. data will be interpolated into
                this range before output.

            Returns: Tuple of (Wavelength, alpha) """

        Wav, k = self.load_k(Lambda=Lambda)

        return (Wav, ((4 * np.pi) / (Wav * 1E-9)) * k)

    def load_temperature(self, Lambda, T=300):
        """ SOPRA_DB.load_temperature(T, Lambda) :: Loads n and k data for a set of materials with temperature dependent
                data sets
            Optional argument T defaults to 300K
            Required argument Lambda specifies a wavelength range and the data is interpolated to fit. This is a
                required argument here as not all data sets in a group are the same length (will be fixed in a
                subsequent update).

            Returns: Tuple of (Wavelength, n, k) """

        T_degC = T - 273.15  # Convert from Kelvin to degC (units given in the data)...

        # Navigate to the correct folder that contains temperature dependent data...
        path = os.path.join(self.__SOPRA_PATH, self.__fname + "_T")

        try:
            os.stat(path)

        except FileNotFoundError:
            print(
                "load_temperature :: WARNING :: Material folder does not exists... Check materials list..."
            )
            # If material folder is not found exit program as nothing more useful can be done...
            sys.exit()

        # if folder exists, read in files from folder...
        Folder = natsorted(os.listdir(path))

        DATA = []
        TEMP = []

        for files in Folder:

            # .DS_Store is a metadata file used in Mac OS X to store various file/ folder info. Ignoring...
            if ".DS_Store" not in files:

                # extract temperature from filename...
                Num = re.findall("[-+]?\d+[\.]?\d*", files)
                Num.append("0")
                TEMP.append(float(Num[0]))

                Wav, n, k = np.genfromtxt(os.path.join(path, files),
                                          delimiter="*",
                                          skip_header=3,
                                          skip_footer=3,
                                          usecols=(2, 3, 4),
                                          unpack=True)

                if Lambda is not None:
                    # Interpolate if the Lambda argument is specified, if not pass loaded Wav, n and k...
                    n_interp = np.interp(Lambda, Wav, n)
                    k_interp = np.interp(Lambda, Wav, k)

                    DATA.append((Lambda, n_interp, k_interp, float(Num[0])))

                else:
                    DATA.append((Wav, n, k, float(Num[0])))

        # Check and see if the entered temperature is within the range of data...
        if T_degC <= min(TEMP):
            print(
                "load_temperature :: WARNING :: Desired Temperature < than the minimum (%6.1f K)"
                % (min(TEMP) + 273.15))
            print("Returned interpolated data will be that at Tmin = %6.1f K" %
                  (min(TEMP) + 273.15))

        elif T_degC >= max(TEMP):
            print(
                "load_temperature :: WARNING :: Desired Temperature > than the maximum (%6.1f K)"
                % (max(TEMP) + 273.15))
            print("Returned interpolated data will be that at Tmax = %6.1f K" %
                  (max(TEMP) + 273.15))

        # use linear interpolation to interpolate the data at the desired temperature...
        n_interp_data = []
        k_interp_data = []

        # In range of all wavelenghs...
        for i in range(0, len(DATA[0][0])):

            T_list = []
            k_at_T = []
            n_at_T = []

            # At each wavelenth, build a list of n and k at each T...
            for X, n, k, temp in DATA:
                T_list.append(temp)
                k_at_T.append(k[i])
                n_at_T.append(n[i])

            # Interpolate the point corresponding to T and build the new data array...
            n_interp_data.append(np.interp(T - 273.19, T_list, n_at_T))
            k_interp_data.append(np.interp(T - 273.19, T_list, k_at_T))

        # Return the Wavelength vector and the new n and k data...
        return (DATA[0][0], n_interp_data, k_interp_data)

    def load_composition(self, Lambda, **kwargs):
        """ SOPRA_DB.load_temperature(T, Lambda) :: Loads n and k data for a set of materials with varying composition.
            Required argument Lambda specifies a wavelength range and the data is interpolated to fit. This is a
                required argument here as not all data sets in a group are the same length (will be fixed in a
                subsequent update).
            Keyword argument :: Specify the factional material and fraction of the desired alloy.

            Returns: Tuple of (Wavelength, n, k) """
        # Use of keyword args allows the user to specify the required material fraction for neatness...
        for material in kwargs:
            mat_fraction = material
            frac = kwargs[material]

        # Navigate to the correct folder that contains temperature dependent data...
        path = os.path.join(self.__SOPRA_PATH,
                            self.__fname + "_" + mat_fraction.upper())
        try:
            os.stat(path)

        except FileNotFoundError:
            print(
                "load_composition :: WARNING :: Material folder does not exists... Check materials list or check"
                + " that composition material is correct...")
            # If material folder is not found exit program as nothing more useful can be done...
            sys.exit()

        # if folder exists, read in files from folder...
        Folder = natsorted(os.listdir(path))

        DATA = []
        COMP = []

        for files in Folder:

            # .DS_Store is a metadata file used in Mac OS X to store various file/ folder info. Ignoring...
            if ".DS_Store" not in files:

                # extract temperature from filename...
                Num = re.findall("[-+]?\d+[\.]?\d*", files)
                Num.append("0")
                COMP.append(float(Num[0]))

                Wav, n, k = np.genfromtxt(os.path.join(path, files),
                                          delimiter="*",
                                          skip_header=3,
                                          skip_footer=3,
                                          usecols=(2, 3, 4),
                                          unpack=True)

                if Lambda is not None:
                    # Interpolate if the Lambda argument is specified, if not pass loaded Wav, n and k...
                    n_interp = np.interp(Lambda, Wav, n)
                    k_interp = np.interp(Lambda, Wav, k)

                    DATA.append((Lambda, n_interp, k_interp, float(Num[0])))

                else:
                    DATA.append((Wav, n, k, float(Num[0])))

        # Check and see if the entered temperature is within the range of data...
        if frac <= min(COMP):
            print(
                "load_composition :: WARNING :: Desired composition < than the minimum (%6.1f %%)"
                % min(COMP))
            print("Returned interpolated data will be that at %6.1f %%" %
                  min(COMP))

        elif frac >= max(COMP):
            print(
                "load_composition :: WARNING :: Desired composition > than the maximum (%6.1f %%)"
                % max(COMP))
            print("Returned interpolated data will be that at %6.1f %%" %
                  max(COMP))

        # use linear interpolation to interpolate the data at the desired temperature...
        n_interp_data = []
        k_interp_data = []

        # In range of all wavelenghs...
        for i in range(0, len(DATA[0][0])):

            x_list = []
            k_at_C = []
            n_at_C = []

            # At each wavelenth, build a list of n and k at each T...
            for X, n, k, x in DATA:
                x_list.append(x)
                k_at_C.append(k[i])
                n_at_C.append(n[i])

            # Interpolate the point corresponding to T and build the new data array...
            n_interp_data.append(np.interp(frac, x_list, n_at_C))
            k_interp_data.append(np.interp(frac, x_list, k_at_C))

        # Return the Wavelength vector and the new n and k data...
        return (DATA[0][0], n_interp_data, k_interp_data)
Пример #24
0
def qe_depletion(junction, options):
    """ Calculates the QE curve of a junction object using the depletion approximation as described in J. Nelson, “The Physics of Solar Cells”, Imperial College Press (2003). The junction is then updated with an "iqe" and several "eqe" functions that calculates the QE curve at any wavelength.

    :param junction: A junction object.
    :param options: Solver options.
    :return: None.
    """

    science_reference(
        'Depletion approximation',
        'J. Nelson, “The Physics of Solar Cells”, Imperial College Press (2003).'
    )

    T = options.T

    # First we have to figure out if we are talking about a PN, NP, PIN or NIP junction
    sn = 0 if not hasattr(junction, "sn") else junction.sn
    sp = 0 if not hasattr(junction, "sp") else junction.sp

    # We search for the emitter and check if it is n-type or p-type
    idx = 0
    pn_or_np = 'pn'

    for layer in junction:
        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 = junction[idx]
            else:
                pRegion = junction[idx]

            id_top = idx

            break

    # Now we check for an intrinsic region and, if there is, for the base.
    if junction[idx + 1].role is 'intrinsic':
        iRegion = junction[idx + 1]

        if junction[idx + 2].role is 'base':
            if pn_or_np == "pn":
                nRegion = junction[idx + 2]
                nidx = idx + 2
            else:
                pRegion = junction[idx + 2]
                pidx = idx + 2

            id_bottom = 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 junction[idx + 1].role is 'base':
        if pn_or_np == "pn":
            nRegion = junction[idx + 1]
            nidx = idx + 1
        else:
            pRegion = junction[idx + 1]
            pidx = idx + 1
        iRegion = None

        id_bottom = idx + 1

    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
    kbT = kb * T
    Egap = nRegion.material.band_gap
    R_shunt = min(junction.R_shunt, 1e14) if hasattr(junction,
                                                     'R_shunt') else 1e14

    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(junction, "dielectric_constant"):
        es = junction.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(junction, "ln"):
        ln = junction.ln
    else:
        ln = pRegion.material.electron_diffusion_length

    if hasattr(junction, "lp"):
        lp = junction.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(junction, "mup"):
        dp = junction.mup * kbT / q
    else:
        dp = pRegion.material.electron_mobility * kbT / q

    if hasattr(junction, "mun"):
        dn = junction.mun * kbT / q
    else:
        dn = nRegion.material.hole_mobility * kbT / 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 = (kbT / q) * np.log(Nd * Na / niSquared) if not hasattr(
        junction, "Vbi") else junction.Vbi  # Jenny p146

    # It's time to calculate the depletion widths
    if not hasattr(junction, "wp") or not hasattr(junction, "wn"):

        if hasattr(
                junction, "depletion_approximation"
        ) and junction.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(junction, "wn") else junction.wn
    wp = wp if not hasattr(junction, "wp") else junction.wp

    w = wn + wp + xi

    # Now it is time to calculate currents
    if pn_or_np == "pn":
        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:
        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

    widths = []
    for layer in junction:
        widths.append(layer.width)

    cum_widths = np.cumsum([0] + widths)

    g = junction.absorbed
    wl = options.wavelength
    wl_sp, ph = options.light_source.spectrum(output_units='photon_flux_per_m',
                                              x=wl)

    # The contribution from the Emitter (top side).
    xa = cum_widths[id_top]
    xb = cum_widths[id_top + 1] - w_top
    deriv = get_J_sc_diffusion_vs_WL(xa,
                                     xb,
                                     g,
                                     d_top,
                                     l_top,
                                     min_top,
                                     s_top,
                                     wl,
                                     ph,
                                     side='top')
    j_sc_top = d_top * abs(deriv)

    # The contribution from the Base (bottom side).
    xa = cum_widths[id_bottom] + w_bottom
    xb = cum_widths[id_bottom + 1]
    deriv = get_J_sc_diffusion_vs_WL(xa,
                                     xb,
                                     g,
                                     d_bottom,
                                     l_bottom,
                                     min_bot,
                                     s_bottom,
                                     wl,
                                     ph,
                                     side='bottom')
    j_sc_bot = d_bottom * abs(deriv)

    # The contribution from the SCR (includes the intrinsic region, if present).
    xa = cum_widths[id_top + 1] - w_top
    xb = cum_widths[id_bottom] + w_bottom
    j_sc_scr = get_J_sc_SCR_vs_WL(xa, xb, g, wl, ph)

    # The total ligth absorbed, but not necesarily collected, is:
    xa = cum_widths[id_top]
    xb = cum_widths[id_bottom + 1]
    zz = np.linspace(xa, xb, 3001)
    gg = g(zz) * ph
    current_absorbed = np.trapz(gg, zz, axis=0)

    # Now, we put everything together
    j_sc = j_sc_top + j_sc_bot + j_sc_scr

    eqe = j_sc / ph
    eqe_emitter = j_sc_top / ph
    eqe_base = j_sc_bot / ph
    eqe_scr = j_sc_scr / ph

    junction.iqe = interp1d(wl, j_sc / current_absorbed)
    junction.eqe = interp1d(wl,
                            eqe,
                            kind='linear',
                            bounds_error=False,
                            assume_sorted=True,
                            fill_value=(eqe[0], eqe[-1]))
    junction.eqe_emitter = interp1d(wl,
                                    eqe_emitter,
                                    kind='linear',
                                    bounds_error=False,
                                    assume_sorted=True,
                                    fill_value=(eqe_emitter[0],
                                                eqe_emitter[-1]))
    junction.eqe_base = interp1d(wl,
                                 eqe_base,
                                 kind='linear',
                                 bounds_error=False,
                                 assume_sorted=True,
                                 fill_value=(eqe_base[0], eqe_base[-1]))
    junction.eqe_scr = interp1d(wl,
                                eqe_scr,
                                kind='linear',
                                bounds_error=False,
                                assume_sorted=True,
                                fill_value=(eqe_scr[0], eqe_scr[-1]))
Пример #25
0
def eight_band_strain_hamiltonian(kx, ky, kz, Ev0, Ec0, exx, ezz, me_eff,
                                  gamma1, gamma2, gamma3, a0, Delta, ac, av, b,
                                  Ep):
    """Hamiltonian to calculate cb, hh, lh, so bands and include strain for the biaxial (along [001]) special case.
    
    See Hamiltonian in ref. but remove row 1 and 6 and column 1 and 6.
    http://prb.aps.org/pdf/PRB/v73/i12/e125348"""
    science_reference(
        "k.p Hamiltonian",
        "Stanko Tomic et al., Electronic structure of InyGa1yAs1xNxGaAs(N) quantum "
        "dots by ten-band k.p theory. Phys. Rev. B 73, 125348 (2006)")

    av = abs(
        av
    )  # The sign in Vurgaftmann is negative while Chuang (and Tomic) assumes it is possitve. We make sure it truly is.

    Eg = Ec0 - Ev0
    sqrt2 = np.sqrt(2)
    sqrt3 = np.sqrt(3)
    sqrt6 = np.sqrt(6)
    sqrt3o2 = np.sqrt(3 / 2)

    m0 = constants.electron_mass

    # The Kane matrix element (Vurgaftman 'interband matrix element')
    P0 = np.sqrt(Ep * constants.hbar**2 / (2 * m0))  # as a momentum

    # "Modified" Luttinger parameters
    gc = 1 / (me_eff / constants.electron_mass) - (Ep / 3) * (2 / Eg + 1 /
                                                              (Eg + Delta))
    g1 = gamma1 - Ep / (
        3 * Eg + Delta
    )  # There was an error here. It was written as: g1 = gamma1 - Ep/(3*(Eg + Delta))
    g2 = gamma2 - Ep / (6 * Eg + 2 * Delta)
    g3 = gamma3 - Ep / (6 * Eg + 2 * Delta)

    Ck = constants.hbar**2 / (2 * m0)

    # Luttinger-Kohn terms
    Ok = lambda kx, ky, kz: Ck * gc * (kx**2 + ky**2 + kz**2)
    Pk = lambda kx, ky, kz: Ck * g1 * (kx**2 + ky**2 + kz**2)
    Qk = lambda kx, ky, kz: Ck * g2 * (kx**2 + ky**2 - 2 * kz**2)
    Rk = lambda kx, ky, kz: Ck * sqrt3 * (g2 * (kx**2 - ky**2) - 2 * 1j * g3 *
                                          kx * ky)
    Sk = lambda kx, ky, kz: Ck * sqrt6 * g3 * (kx - 1j * ky) * kz
    Tk = lambda kx, ky, kz: 1 / sqrt6 * P0 * (kx + 1j * ky)
    Uk = lambda kx, ky, kz: 1 / sqrt3 * P0 * kz

    # Conjugate
    Rkc = lambda kx, ky, kz: Ck * sqrt3 * (g2 * (kx**2 - ky**2) + 2 * 1j * g3 *
                                           kx * ky)
    Skc = lambda kx, ky, kz: Ck * sqrt6 * g3 * (kx + 1j * ky) * kz
    Tkc = lambda kx, ky, kz: 1 / sqrt6 * P0 * (kx - 1j * ky)

    # Strain terms (biaxial approximation makes many of these function zero)
    Oe = lambda exx, eyy, ezz: ac * (exx + eyy + ezz)
    Pe = lambda exx, eyy, ezz: -av * (exx + eyy + ezz)
    Qe = lambda exx, eyy, ezz: -b / 2 * (exx + eyy - 2 * ezz)
    # Re = lambda exx,eyy,ezz: 0
    # Se = lambda exx,eyy,ezz: 0
    # Tk = lambda exx,eyy,ezz: 0
    # Uk = lambda exx,eyy,ezz: 0

    # Complete terms
    O = lambda kx, ky, kz, exx, eyy, ezz: Ok(kx, ky, kz) + Oe(exx, eyy, ezz)
    P = lambda kx, ky, kz, exx, eyy, ezz: Pk(kx, ky, kz) + Pe(exx, eyy, ezz)
    Q = lambda kx, ky, kz, exx, eyy, ezz: Qk(kx, ky, kz) + Qe(exx, eyy, ezz)
    R = lambda kx, ky, kz, exx, eyy, ezz: Rk(kx, ky, kz
                                             )  # Here the strain term is zero
    S = lambda kx, ky, kz, exx, eyy, ezz: Sk(kx, ky, kz
                                             )  # Here the strain term is zero
    T = lambda kx, ky, kz, exx, eyy, ezz: Tk(kx, ky, kz
                                             )  # Here the strain term is zero
    U = lambda kx, ky, kz, exx, eyy, ezz: Uk(kx, ky, kz
                                             )  # Here the strain term is zero

    # Conjugates
    Rc = lambda kx, ky, kz, exx, eyy, ezz: Rkc(
        kx, ky, kz)  # Here the strain term is zero
    Sc = lambda kx, ky, kz, exx, eyy, ezz: Skc(
        kx, ky, kz)  # Here the strain term is zero
    Tc = lambda kx, ky, kz, exx, eyy, ezz: Tkc(
        kx, ky, kz)  # Here the strain term is zero

    # Bands
    Ecb = lambda kx, ky, kz, exx, eyy, ezz: Ec0 + O(kx, ky, kz, exx, eyy, ezz)
    Ehh = lambda kx, ky, kz, exx, eyy, ezz: Ev0 - (P(
        kx, ky, kz, exx, eyy, ezz) + Q(kx, ky, kz, exx, eyy, ezz))
    Elh = lambda kx, ky, kz, exx, eyy, ezz: Ev0 - (P(
        kx, ky, kz, exx, eyy, ezz) - Q(kx, ky, kz, exx, eyy, ezz))
    Eso = lambda kx, ky, kz, exx, eyy, ezz: Ev0 - (P(kx, ky, kz, exx, eyy, ezz)
                                                   + Delta)

    eyy = exx
    p = P(kx, ky, kz, exx, eyy, ezz)
    q = Q(kx, ky, kz, exx, eyy, ezz)
    r = R(kx, ky, kz, exx, eyy, ezz)
    s = S(kx, ky, kz, exx, eyy, ezz)
    t = T(kx, ky, kz, exx, eyy, ezz)
    u = U(kx, ky, kz, exx, eyy, ezz)
    rc = Rc(kx, ky, kz, exx, eyy, ezz)
    sc = Sc(kx, ky, kz, exx, eyy, ezz)
    tc = Tc(kx, ky, kz, exx, eyy, ezz)
    cb = Ecb(kx, ky, kz, exx, eyy, ezz)
    hh = Ehh(kx, ky, kz, exx, eyy, ezz)
    lh = Elh(kx, ky, kz, exx, eyy, ezz)
    so = Eso(kx, ky, kz, exx, eyy, ezz)

    H_ST = np.mat(
        [[cb, -sqrt3 * t, sqrt2 * u, -u, 0, 0, -tc, -sqrt2 * tc],
         [-sqrt3 * tc, hh, sqrt2 * s, -s, 0, 0, -r, -sqrt2 * r],
         [sqrt2 * u, sqrt2 * sc, lh, -sqrt2 * q, tc, r, 0, sqrt3 * s],
         [-u, -sc, -sqrt2 * q, so, sqrt2 * tc, sqrt2 * r, -sqrt3 * s, 0],
         [0, 0, t, sqrt2 * t, cb, -sqrt3 * tc, sqrt2 * u, -u],
         [0, 0, rc, sqrt2 * rc, -sqrt3 * t, hh, sqrt2 * sc, -sc],
         [-t, -rc, 0, -sqrt3 * sc, sqrt2 * u, sqrt2 * s, lh, -sqrt2 * q],
         [-sqrt2 * t, -sqrt2 * rc, sqrt3 * sc, 0, -u, -s, -sqrt2 * q, so]])

    #     View hamiltonian, good for debugging
    #     if True:
    #         import pylab
    #         pylab.imshow(numpy.real(H_ST))
    #         pylab.show()

    H_ST = H_ST.transpose()
    E, Psi = eig(H_ST)  # do all the math

    bands = np.array((sorted(E.real)))
    return bands
Пример #26
0
    def E_ID(self, energy, material_parameters, **kwargs):
        """
        Custom_CPPB.Eg_ID() :: From Adachi's formalism, contributions to the complex dielectric function from the indirect
            band-gap transitions.

        :param energy: energy: Energy array (eV).
        :param material_parameters: Parameter set imported using Material_Parameters() method. Not required as long as
            keyword arguments are specified.
        :param kwargs: These take in individual parameters for the model. Keywords should take the following form;
                    Gamma_Eg_ID
                    Alpha_Eg_ID
                    Eg_ED
                    Ec
                    D

        :return: Indirect band gap contributions to the complex dielectric function.
        """

        # Scientific reference for this work...
        science_reference(
            "Sadao Adachi, Physical Properties of III-V Semiconductor Compounds",
            "Adachi, S., Physical Properties of III-V Semiconductor Compounds, John Wiley & Sons (1992)"
        )

        # Conditional statement determining where the input parameters come from...
        if material_parameters is None and bool(kwargs) is False:

            raise ValueError("No material parameters specified...")

        elif material_parameters is not None and bool(kwargs) is False:

            Params = material_parameters

        elif material_parameters is not None and bool(kwargs) is True:

            Params = material_parameters

            for key in kwargs:

                try:

                    Params[key] = kwargs[key]
                except KeyError:
                    print("Invalid material parameter...")

        elif bool(kwargs) is True:

            Params = {}

            for key in kwargs:

                try:

                    Params[key] = kwargs[key]
                except KeyError:
                    print("Invalid material parameter...")

        # Frequency dependent broadening parameter...
        Gamma = self.Broad(Params["Gamma_Eg_ID"], Params["Alpha_Eg_ID"],
                           Params["Eg_ID"], energy)

        # Function describing the contributions of indirect transitions...
        term1 = -1 * ((
            (Params["Eg_ID"]**2) /
            (energy + 1j * Gamma)**2) * np.log(Params["Ec"] / Params["Eg_ID"]))
        term2 = 0.5 * ((1 + (Params["Eg_ID"] / (energy + 1j * Gamma))) ** 2) * \
                np.log((energy + 1j * Gamma + Params["Ec"]) / (energy + 1j * Gamma + Params["Eg_ID"]))
        term3 = 0.5 * ((1 - (Params["Eg_ID"] / (energy + 1j * Gamma))) ** 2) * \
                np.log((energy + 1j * Gamma - Params["Ec"]) / (energy + 1j * Gamma - Params["Eg_ID"]))

        Eps = (2 * Params["D"] / np.pi) * (term1 + term2 + term3)

        # Additional line to address change in phase of the imaginary signal.
        return Eps.real + 1j * abs(Eps.imag)
Пример #27
0
def optimise(well_material, well_thickness, well_fraction, barrier_material, barrier_thickness, barrier_fraction,
        lattice_material, lattice_material_fraction=0, parameter_to_optimise="well_thickness", T=300):
    """ This can't handle sol-core materials yet. Needs layers."""

    options = ["barrier_thickness", "well_thickness", "barrier_fraction", "well_fraction"]
    if parameter_to_optimise not in options:
        print('ERROR: Wring option in strain_balancing.optimise. '
              'The only valid options are: {}, {}, {}, {}'.format(*options))

    science_reference("strain-balancing",
                      "Ekins-Daukes et al. Strain-balanced Criteria for Multiple Quantum Well Structures and Its "
                      "Signature in X-ray Rocking Curves. Crystal growth & design (2002) vol. 2 (4) pp. 287-292")

    get_vurgaftman_parameter = ParameterSystem().get_parameter
    t1 = well_thickness
    t2 = barrier_thickness

    x1 = well_fraction
    x2 = barrier_fraction

    C11_well = get_vurgaftman_parameter(well_material, "c11", x=x1, T=T)
    C12_well = get_vurgaftman_parameter(well_material, "c12", x=x1, T=T)
    A1 = C11_well + C12_well - 2 * C12_well ** 2 / C11_well
    a1 = get_vurgaftman_parameter(well_material, "lattice_constant", x=x1, T=T)

    C11_barrier = get_vurgaftman_parameter(barrier_material, "c11", x=x2, T=T)
    C12_barrier = get_vurgaftman_parameter(barrier_material, "c12", x=x2, T=T)
    A2 = C11_barrier + C12_barrier - 2 * C12_barrier ** 2 / C11_barrier
    a2 = get_vurgaftman_parameter(barrier_material, "lattice_constant", x=x2, T=T)

    a0 = get_vurgaftman_parameter(lattice_material, "lattice_constant", x=lattice_material_fraction, T=T)

    a0 = convert(a0, "angstrom", "m")
    a1 = convert(a1, "angstrom", "m")
    a2 = convert(a2, "angstrom", "m")
    if parameter_to_optimise == "barrier_thickness":
        t2 = - (A1 * t1 * a2 ** 2 * (a0 - a1)) / (A2 * a1 ** 2 * (a0 - a2))
    if parameter_to_optimise == "well_thickness":
        t1 = - (A2 * t2 * a1 ** 2 * (a0 - a2)) / (A1 * a2 ** 2 * (a0 - a1))

    if parameter_to_optimise == "well_fraction":
        def func(x):
            C11_well = get_vurgaftman_parameter(well_material, "c11", x=x, T=T)
            C12_well = get_vurgaftman_parameter(well_material, "c12", x=x, T=T)
            A1 = C11_well + C12_well - 2 * C12_well ** 2 / C11_well
            a1 = get_vurgaftman_parameter(well_material, "lattice_constant", x=x, T=T)
            a1 = convert(a1, "angstrom", "m")

            misfit = - (A1 * t1 * a2 ** 2 * (a0 - a1)) / (A2 * a1 ** 2 * (a0 - a2)) - t2
            return misfit

        print(func(linspace(0, 1, 10)))

        x1 = bisect(func, 0, 1)

    if parameter_to_optimise == "barrier_fraction":
        def func(x):
            C11_barrier = get_vurgaftman_parameter(barrier_material, "c11", x=x, T=T)
            C12_barrier = get_vurgaftman_parameter(barrier_material, "c12", x=x, T=T)
            A2 = C11_barrier + C12_barrier - 2 * C12_barrier ** 2 / C11_barrier
            a2 = get_vurgaftman_parameter(barrier_material, "lattice_constant", x=x, T=T)
            a2 = convert(a2, "angstrom", "m")

            if a0 == a2 and x == 0:
                a2 = get_vurgaftman_parameter(barrier_material, "lattice_constant", x=0.001, T=T)
                a2 = convert(a2, "angstrom", "m")

            misfit = - (A1 * t1 * a2 ** 2 * (a0 - a1)) / (A2 * a1 ** 2 * (a0 - a2)) - t2
            return misfit

        x2 = bisect(func, 0, 1)

    return {
        "well_material": well_material,
        "well_thickness": t1,
        "well_fraction": x1,
        "barrier_material": barrier_material,
        "barrier_thickness": t2,
        "barrier_fraction": x2,
        "lattice_material": lattice_material,
        "lattice_fraction": lattice_material_fraction
    }
Пример #28
0
import solcore.science_tracker as st

st.science_reference(
    'GaP and InP refractive index',
    'S. Adachi. Optical dispersion relations for GaP, GaAs, GaSb, InP, InAs, InSb, AlxGa1−xAs, and In1−xGaxAsyP1−y, J. Appl. Phys. 66, 6030-6040 (1989)'
)
st.science_reference(
    'GaInP refractive index',
    'M. Schubert, V. Gottschalch, C. M. Herzinger, H. Yao, P. G. Snyder and J. A. Woollam. Optical constants of GaxIn1−xP lattice matched to GaAs, J. Appl. Phys. 77, 3416 (1995)'
)
Пример #29
0
def iv_depletion(junction, options):
    """ Calculates the IV curve of a junction object using the depletion approximation as described in J. Nelson, “The Physics of Solar Cells”, Imperial College Press (2003). The junction is then updated with an "iv" function that calculates the IV curve at any voltage.

    :param junction: A junction object.
    :param options: Solver options.
    :return: None.
    """

    science_reference(
        'Depletion approximation',
        'J. Nelson, “The Physics of Solar Cells”, Imperial College Press (2003).'
    )

    junction.voltage = options.internal_voltages
    T = options.T

    # First we have to figure out if we are talking about a PN, NP, PIN or NIP junction
    sn = 0 if not hasattr(junction, "sn") else junction.sn
    sp = 0 if not hasattr(junction, "sp") else junction.sp

    # We search for the emitter and check if it is n-type or p-type
    idx = 0
    pn_or_np = 'pn'

    for layer in junction:
        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 = junction[idx]
            else:
                pRegion = junction[idx]

            id_top = idx

            break

    # Now we check for an intrinsic region and, if there is, for the base.
    if junction[idx + 1].role is 'intrinsic':
        iRegion = junction[idx + 1]

        if junction[idx + 2].role is 'base':
            if pn_or_np == "pn":
                nRegion = junction[idx + 2]
                nidx = idx + 2
            else:
                pRegion = junction[idx + 2]
                pidx = idx + 2

            id_bottom = 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 junction[idx + 1].role is 'base':
        if pn_or_np == "pn":
            nRegion = junction[idx + 1]
            nidx = idx + 1
        else:
            pRegion = junction[idx + 1]
            pidx = idx + 1
        iRegion = None

        id_bottom = idx + 1

    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
    kbT = kb * T
    Egap = nRegion.material.band_gap
    R_shunt = min(junction.R_shunt, 1e14) if hasattr(junction,
                                                     'R_shunt') else 1e14

    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(junction, "dielectric_constant"):
        es = junction.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(junction, "ln"):
        ln = junction.ln
    else:
        ln = pRegion.material.electron_diffusion_length

    if hasattr(junction, "lp"):
        lp = junction.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(junction, "mup"):
        dp = junction.mup * kbT / q
    else:
        dp = pRegion.material.electron_mobility * kbT / q

    if hasattr(junction, "mun"):
        dn = junction.mun * kbT / q
    else:
        dn = nRegion.material.hole_mobility * kbT / 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 = (kbT / q) * np.log(Nd * Na / niSquared) if not hasattr(
        junction, "Vbi") else junction.Vbi  # Jenny p146

    # And now we account for the possible applied voltage, which can be, at most, equal to Vbi
    V = np.where(junction.voltage < Vbi - 0.001, junction.voltage, Vbi - 0.001)

    # It's time to calculate the depletion widths
    if not hasattr(junction, "wp") or not hasattr(junction, "wn"):

        if hasattr(
                junction, "depletion_approximation"
        ) and junction.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 - V) / (q * Na))
            wn = np.sqrt(2 * es * (Vbi - V) / (q * Nd))

        else:
            wn = (-xi + np.sqrt(xi**2 + 2. * es * (Vbi - V) / q *
                                (1 / Na + 1 / Nd))) / (1 + Nd / Na)
            wp = (-xi + np.sqrt(xi**2 + 2. * es * (Vbi - V) / q *
                                (1 / Na + 1 / Nd))) / (1 + Na / Nd)

    wn = wn if not hasattr(junction, "wn") else junction.wn
    wp = wp if not hasattr(junction, "wp") else junction.wp

    w = wn + wp + xi

    # Now it is time to calculate currents
    if pn_or_np == "pn":
        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:
        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

    JtopDark = get_j_top(x_top, w_top, l_top, s_top, d_top, V, min_top, T)
    JbotDark = get_j_bot(x_bottom, w_bottom, l_bottom, s_bottom, d_bottom, V,
                         min_bot, T)

    # hereby we define the subscripts to refer to the layer in which the current is generated:
    if pn_or_np == "pn":
        JnDark, JpDark = JbotDark, JtopDark
    else:
        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

    # Here we use the full version of the SRH recombination term as calculated by Sah et al. Works for positive bias
    # and moderately negative ones.
    science_reference(
        'SRH current term.',
        'C. T. Sah, R. N. Noyce, and W. Shockley, “Carrier Generation and Recombination in P-N Junctions and P-N Junction Characteristics,” presented at the Proceedings of the IRE, 1957, vol. 45, no. 9, pp. 1228–1243.'
    )
    Jrec = get_Jsrh(ni, V, Vbi, lifetime_p, lifetime_n, w, kbT)

    J_sc_top = 0
    J_sc_bot = 0
    J_sc_scr = 0

    if options.light_iv:

        widths = []
        for layer in junction:
            widths.append(layer.width)

        cum_widths = np.cumsum([0] + widths)

        g = junction.absorbed
        wl = options.wavelength
        wl_sp, ph = options.light_source.spectrum(
            output_units='photon_flux_per_m', x=wl)
        id_v0 = np.argmin(abs(V))

        # The contribution from the Emitter (top side).
        xa = cum_widths[id_top]
        xb = cum_widths[id_top + 1] - w_top[id_v0]
        deriv = get_J_sc_diffusion(xa,
                                   xb,
                                   g,
                                   d_top,
                                   l_top,
                                   min_top,
                                   s_top,
                                   wl,
                                   ph,
                                   side='top')
        J_sc_top = q * d_top * abs(deriv)

        # The contribution from the Base (bottom side).
        xa = cum_widths[id_bottom] + w_bottom[id_v0]
        xb = cum_widths[id_bottom + 1]
        deriv = get_J_sc_diffusion(xa,
                                   xb,
                                   g,
                                   d_bottom,
                                   l_bottom,
                                   min_bot,
                                   s_bottom,
                                   wl,
                                   ph,
                                   side='bottom')
        J_sc_bot = q * d_bottom * abs(deriv)

        # The contribution from the SCR (includes the intrinsic region, if present).
        xa = cum_widths[id_top + 1] - w_top[id_v0]
        xb = cum_widths[id_bottom] + w_bottom[id_v0]
        J_sc_scr = q * get_J_sc_SCR(xa, xb, g, wl, ph)

    # And, finally, we output the currents
    junction.current = Jrec + JnDark + JpDark + V / R_shunt - J_sc_top - J_sc_bot - J_sc_scr
    junction.iv = interp1d(junction.voltage,
                           junction.current,
                           kind='linear',
                           bounds_error=False,
                           assume_sorted=True,
                           fill_value=(junction.current[0],
                                       junction.current[-1]))
    junction.region_currents = State({
        "Jn_dif": JnDark,
        "Jp_dif": JpDark,
        "Jscr_srh": Jrec,
        "J_sc_top": J_sc_top,
        "J_sc_bot": J_sc_bot,
        "J_sc_scr": J_sc_scr
    })
Пример #30
0
import solcore.science_tracker as st

st.science_reference(
    'InP refractive index',
    'S. Adachi. Optical dispersion relations for GaP, GaAs, GaSb, InP, InAs, InSb, AlxGa1−xAs, and In1−xGaxAsyP1−y, J. Appl. Phys. 66, 6030-6040 (1989)'
)
st.science_reference(
    'AlInP refractive index',
    'E. Ochoa-Martínez, L. Barrutia, M. Ochoa, E. Barrigón, I. García, I. Rey-Stolle, C. Algora, P. Basa, G. Kronome, and M. Gabás, “Refractive indexes and extinction coefficients of n- and p-type doped GaInP, AlInP and AlGaInP for multijunction solar cells,” Solar Energy Materials and Solar Cells, vol. 174, pp. 388–396, Jan. 2018.'
)
st.science_reference(
    'AlP refractive index. Cut to bandgap.',
    'V. Emberger, F. Hatami, W. Ted Masselink, and S. Peters, “AlP/GaP distributed Bragg reflectors,” Appl. Phys. Lett., vol. 103, no. 3, pp. 031101–4, Jul. 2013.'
)