Source code for solcore.optics.tmm

import types
from typing import List, Optional

import numpy as np
from numpy.typing import NDArray

from ..absorption_calculator import (
    OptiStack,
    calculate_absorption_profile,
    calculate_rat,
)
from ..registries import register_optics
from ..solar_cell import SolarCell
from ..structure import Junction, Layer, TunnelJunction


[docs]@register_optics(name="TMM") def solve_tmm( solar_cell: SolarCell, wavelength: NDArray, position: NDArray, BL_correction: bool = True, theta: float = 0.0, pol: str = "u", zero_threshold: float = 1e-5, no_back_reflection: bool = True, coherency_list: Optional[List[str]] = None, **kwargs ) -> None: """Calculates the RAT of a solar cell object using the transfer matrix method. Internally, it creates an OptiStack and then it calculates the optical properties of the whole structure. A substrate can be specified in the SolarCell object, which is treated as a semi-infinite transmission medium. Shading can also be specified (as a fraction). A coherency_list option can be provided: Args: solar_cell: A solar_cell object wavelength: Array of wavelegth at which the optics are calculated. position: Array of positions in the z direction to calculate the absorption vs depth. BL_correction: If is set to True, thick layers (thickness > 10*maximum wavelength) are treated incoherently using the Beer-Lambert law, to avoid the calculation of unphysical interference oscillations in the R/A/T spectra. theta: the polar incidence angle, in degrees, with 0 degrees being normal incidence. pol: the polarization of the light ('s', 'p' or 'u') zero_threshold: when the fraction of incident light absorbed in a layer is less than this value, the absorption profile is completely set to zero for both coherent and incoherent calculations. This is applied on a wavelength-by-wavelength basis and is intended to prevent errors where integrating a weak absorption profile in a layer over many points leads to calculated EQE > total absorption in that layer. no_back_reflection: Sets whether reflections from the back surface are suppressed (if set to True, the default), or taken into account (if set to False). coherency_list: If present, this should have the same number of elements than number of layers (if a Junction contains multiple Layers, each should have its own entry in the coherency list). Each element is either 'c' for coherent treatment of that layer or 'i' for incoherent treatment. Return: None """ # We include the shadowing losses initial = (1 - solar_cell.shading) if hasattr(solar_cell, "shading") else 1 # Now we calculate the absorbed and transmitted light. We first get all the relevant # parameters from the objects all_layers = [] widths = [] n_layers_junction = [] for j, 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: all_layers.append(layer_object) widths.append(layer_object.width) n_layers_junction.append(1) # For each junction, and layer within the junction, we get the absorption # coefficient and the layer width. elif type(layer_object) in [TunnelJunction, Junction]: n_layers_junction.append(len(layer_object)) for i, layer in enumerate(layer_object): all_layers.append(layer) widths.append(layer.width) # With all the information, we create the optical stack full_stack = OptiStack( all_layers, no_back_reflection=no_back_reflection, substrate=solar_cell.substrate, incidence=solar_cell.incidence, ) if coherency_list is not None: coherent = False if len(coherency_list) != full_stack.num_layers: raise ValueError( "Error: The coherency list must have as many elements (now {}) as the " "number of layers (now {}).".format( len(coherency_list), full_stack.num_layers ) ) else: coherent = True # assume it's safe to ignore interference effects if BL_correction and any(widths > 10 * np.max(wavelength)): make_incoherent = np.where(np.array(widths) > 10 * np.max(wavelength))[0] print("Treating layer(s) " + str(make_incoherent).strip("[]") + " incoherently") if not coherency_list: coherency_list_ = np.array(len(all_layers) * ["c"]) coherent = False else: coherency_list_ = np.array(coherency_list) coherency_list_[make_incoherent] = "i" coherency_list = coherency_list_.tolist() position = position * 1e9 profile_position = position[position < sum(full_stack.widths)] print("Calculating RAT...") RAT = calculate_rat( full_stack, wavelength * 1e9, angle=theta, coherent=coherent, coherency_list=coherency_list, no_back_reflection=no_back_reflection, pol=pol, ) print("Calculating absorption profile...") out = calculate_absorption_profile( full_stack, wavelength * 1e9, dist=profile_position, angle=theta, no_back_reflection=no_back_reflection, pol=pol, coherent=coherent, coherency_list=coherency_list, zero_threshold=zero_threshold, RAT_out=RAT, ) # With all this information, we are ready to calculate the differential absorption # function diff_absorption, all_absorbed = calculate_absorption_tmm(out, initial) # Each building block (layer or junction) needs to have access to the absorbed light # in its region. We update each object with that information. # first entry is R, last entry is T layer = 0 A_per_layer = np.array(RAT["A_per_layer"][1:-1]) for j in range(len(solar_cell)): solar_cell[j].layer_absorption = initial * np.sum( A_per_layer[layer : (layer + n_layers_junction[j])], axis=0 ) solar_cell[j].diff_absorption = diff_absorption solar_cell[j].absorbed = types.MethodType(absorbed, solar_cell[j]) layer = layer + n_layers_junction[j] solar_cell.reflected = RAT["R"] * initial solar_cell.absorbed = sum( [solar_cell[x].layer_absorption for x in np.arange(len(solar_cell))] ) solar_cell.transmitted = initial - solar_cell.reflected - solar_cell.absorbed
[docs]def absorbed(self, z): out = self.diff_absorption(self.offset + z) * (z < self.width) return out.T
[docs]def calculate_absorption_tmm(tmm_out, initial=1): all_z = tmm_out["position"] * 1e-9 all_abs = initial * tmm_out["absorption"] / 1e-9 def diff_absorption(z): idx = all_z.searchsorted(z) idx = np.where(idx <= len(all_z) - 1, idx, len(all_z) - 1) idx = np.where(idx > 0, idx, 1) try: z1 = all_z[idx - 1] z2 = all_z[idx] f = np.divide(z - z1, z2 - z1, out=np.zeros_like(z), where=np.abs(z2-z1) > 1e-12) # this is to avoid divide by zero errors (|f| gets very large) when z1 = z2 out = (1-f) * all_abs[:, idx - 1] + f * all_abs[:, idx] except IndexError: out = all_abs[:, idx] return out all_absorbed = np.trapz(diff_absorption(all_z), all_z) return diff_absorption, all_absorbed