Source code for solcore.optics.beer_lambert

import types

import numpy as np
from numpy.typing import NDArray
from scipy.interpolate import interp1d

from .. import analytic_solar_cells as ASC
from ..registries import register_optics
from ..solar_cell import SolarCell
from ..structure import Junction, Layer, TunnelJunction


[docs]@register_optics(name="BL") def solve_beer_lambert(solar_cell: SolarCell, wavelength: NDArray, **kwargs) -> None: """Calculates RAT of a solar cell object using the Beer-Lambert law. Reflection is not really calculated and needs to be provided externally, otherwise it is assumed to be zero. Args: - solar_cell: A solar_cell object - wavelength: Array of wavelegth at which the optics are calculated. Return: None """ solar_cell.wavelength = wavelength fraction = np.ones(wavelength.shape) # We include the shadowing losses if hasattr(solar_cell, "shading"): fraction *= 1 - solar_cell.shading # And the reflexion losses if hasattr(solar_cell, "reflectivity") and solar_cell.reflectivity is not None: solar_cell.reflected = solar_cell.reflectivity(wavelength) fraction *= 1 - solar_cell.reflected else: solar_cell.reflected = np.zeros(fraction.shape) # Now we calculate the absorbed and transmitted light. We first get all the relevant # parameters from the objects widths = [] alphas = [] 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: widths.append(layer_object.width) alphas.append(layer_object.material.alpha(wavelength)) n_layers_junction.append(1) # For each Tunnel junctions will have, at most, a resistance an some layers # absorbing light. elif type(layer_object) is TunnelJunction: n_layers_junction.append(len(layer_object)) for i, layer in enumerate(layer_object): widths.append(layer.width) alphas.append(layer.material.alpha(wavelength)) # For each junction, and layer within the junction, we get the absorption # coeficient and the layer width. elif type(layer_object) is Junction: n_layers_junction.append(len(layer_object)) kind = solar_cell[j].kind if hasattr(solar_cell[j], "kind") else None if kind == "2D": # If the junction has a Jsc or EQE already defined, we ignore that # junction in the optical calculation if hasattr(solar_cell[j], "jsc") or hasattr(solar_cell[j], "eqe"): print( "Warning: A junction of kind '2D' found. Junction ignored in " "the optics calculation!" ) w = layer_object.width def alf(x): return 0.0 * x solar_cell[j].alpha = alf solar_cell[j].reflected = interp1d( wavelength, solar_cell.reflected, bounds_error=False, fill_value=(0, 0), ) widths.append(w) alphas.append(alf(wavelength)) # Otherwise, we try to treat is as a DB junction from the optical point # of view else: ASC.absorptance_detailed_balance(solar_cell[j]) w = layer_object.width def alf(x): val = np.maximum(1 - layer_object.absorptance(x), 1e-3) return -1 / w * np.log(val) solar_cell[j].alpha = alf solar_cell[j].reflected = interp1d( wavelength, solar_cell.reflected, bounds_error=False, fill_value=(0, 0), ) widths.append(w) alphas.append(alf(wavelength)) elif kind == "DB": ASC.absorptance_detailed_balance(solar_cell[j]) w = layer_object.width def alf(x): val = np.maximum(1 - layer_object.absorptance(x), 1e-3) return -1 / w * np.log(val) solar_cell[j].alpha = alf solar_cell[j].reflected = interp1d( wavelength, solar_cell.reflected, bounds_error=False, fill_value=(0, 0), ) widths.append(w) alphas.append(alf(wavelength)) else: for i, layer in enumerate(layer_object): widths.append(layer.width) alphas.append(layer.material.alpha(wavelength)) # With all this information, we are ready to calculate the absorbed light diff_absorption, transmitted, all_absorbed = calculate_absorption_beer_lambert( widths, alphas, fraction ) # Each building block (layer or junction) needs to have access to the absorbed light # in its region. # We update each object with that information. # need the *1 because we DO NOT want to modify fraction! Will mess up profile # calculation I0 = 1 * fraction layers_above_offset = np.cumsum([0] + n_layers_junction) for j in range(len(solar_cell)): solar_cell[j].diff_absorption = diff_absorption solar_cell[j].absorbed = types.MethodType(absorbed, solar_cell[j]) # total absorption at each wavelength, per layer A_junc = np.zeros_like(wavelength) for k in range(n_layers_junction[j]): ilayer = layers_above_offset[j] + k A_layer = I0 * (1 - np.exp(-alphas[ilayer] * widths[ilayer])) A_junc += A_layer I0 -= A_layer solar_cell[j].layer_absorption = A_junc solar_cell.transmitted = transmitted solar_cell.absorbed = all_absorbed
[docs]def absorbed(self, z): out = self.diff_absorption(self.offset + z).T * (z < self.width) return out.T
[docs]def calculate_absorption_beer_lambert(widths, alphas, fraction): # Number of spectral elements N = len(alphas[0]) cum_widths = np.cumsum([0] + widths) widths = np.array(widths) # At any given position, the absorption per unit length is alpha * exp(-alpha*z) but # we have to remove all light # absorbed above it: OD = [np.zeros(N)] for i in range(len(widths)): OD.append(OD[i] + alphas[i] * widths[i]) # After getting al the alphas and widths, we need to create a function that takes as # arguments the depth z and # returns the differential fraction of absorbed light at that position. def diff_absorption(z): # First we find to which layer a given z belong idx = cum_widths.searchsorted(z) - 1 idx = np.maximum(idx, 0) # Now the distance to the begining of that layer z_local = z - cum_widths[idx] # And finally, we calculate a 2D array of the absorption per unit length vs # position and wavelength output = np.zeros((len(z), N)) inside = np.sum(z < cum_widths[-1]) for k in range(inside): loc = idx[k] output[k] = alphas[loc] * np.exp(-OD[loc] - alphas[loc] * z_local[k]) return output * fraction all_absorbed = (1 - np.exp(-OD[-1])) * fraction trans = np.exp(-OD[-1]) * fraction return diff_absorption, trans, all_absorbed
[docs]def calculate_absorptance_PDD_and_DA(junction, wl, fraction): widths = [] alphas = [] for i, layer in enumerate(junction): try: widths.append(layer.width) alphas.append(layer.material.alpha(wl)) except Exception as err: # We are, most likely, in the presence of a QW structure. It needs to be # already processed, otherwise the calculation will fail. # We skip this part, for now. print(err.args) raise cum_widths = np.cumsum([0] + widths[:-1]) widths = np.array(widths) # At any given position, the absorption per unit length is alpha * exp(alpha*z) but # we have to remove all light # absorbed above it: OD = [np.zeros(alphas[0].shape)] for i in range(len(widths) - 1): OD.append(OD[i] + alphas[i] * widths[i]) # After getting al the alphas and widths, we need to create a function that takes as # arguments the depth z and returns the differential fraction of absorbed light at # that position. def diff_absorption(z): # First we find to which layer a given z belong idx = cum_widths.searchsorted(z) - 1 idx = np.maximum(idx, 0) # Now the distance to the begining of that layer z_local = z - cum_widths[idx] # If we go beyond the limit of the function, the absorption must be zero, so: too_far = 1 * (z < (cum_widths[-1] + widths[-1])) # And finally, we calculate a 2D array of the absorption per unit lenght vs # position and wavelength output = np.zeros((len(z), len(wl))) for i in range(len(z) - 1): loc = idx[i] output[i] = alphas[loc] * np.exp(-OD[loc] - alphas[loc] * z_local[i]) output[i] = output[i] * too_far[i] return output * fraction trans = np.exp(-OD[-1] - alphas[-1] * widths[-1]) return diff_absorption, trans
[docs]def calculate_absorptance_DB(junction, wl, fraction): # First, we calculate the absorptance of the cell ASC.absorptance_detailed_balance(junction) def absorption(new_wl): # The fraction of light absorbed in the junction f = np.interp(new_wl, wl, fraction, left=fraction[0], right=fraction[-1]) return junction.absorptance(new_wl) * f trans = absorption(wl) return absorption, trans