def solve_tmm(solar_cell, options): """ Calculates the reflection, transmission and absorption 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. :param solar_cell: A solar_cell object :param options: Options for the solver :return: None """ wl = options.wavelength # 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 = [] 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) # For each junction, and layer within the junction, we get the absorption coefficient and the layer width. elif type(layer_object) in [TunnelJunction, Junction]: for i, layer in enumerate(layer_object): all_layers.append(layer) # With all the information, we create the optical stack no_back_reflexion = options.no_back_reflexion if 'no_back_reflexion' in options.keys() else True stack = OptiStack(all_layers, no_back_reflexion=no_back_reflexion) #dist = np.logspace(-1, np.log10(solar_cell.width * 1e9), int(1000 * np.log10(solar_cell.width * 1e9))) dist = np.arange(0, solar_cell.width * 1e9, 1) # print(len(dist)) position = options.position if 'position' in options.keys() else dist print('Calculating RAT...') RAT = calculate_rat(stack, wl * 1e9, coherent=True) print('Calculating absorption profile...') out = calculate_absorption_profile(stack, wl * 1e9, dist=position) # With all this information, we are ready to calculate the differential absorption function diff_absorption, all_absorbed = calculate_absorption_tmm(out) # Each building block (layer or junction) needs to have access to the absorbed light in its region. # We update each object with that information. for j in range(len(solar_cell)): solar_cell[j].diff_absorption = diff_absorption solar_cell[j].absorbed = types.MethodType(absorbed, solar_cell[j]) solar_cell.reflected = RAT['R'] * initial solar_cell.transmitted = (1 - RAT['R'] - all_absorbed) * initial solar_cell.absorbed = all_absorbed * initial
def __init__(self, stack, coherent=True, coherency_list=None, no_back_reflection=False): """ Set up structure for TMM calculations :param stack: an OptiStack object. :param wavelength: Wavelengths (in nm) in which calculate the data. An array. :param angle: Angle (in radians) of the incident light. Default: 0 (normal incidence). :param pol: Polarisation of the light: 's', 'p' or 'u'. Default: 'u' (unpolarised). :param coherent: If the light is coherent or not. If not, a coherency list must be added. :param coherency_list: A list indicating in which layers light should be treated as coeherent ('c') and in which incoherent ('i'). It needs as many elements as layers in the structure. :param profile: whether or not to calculate the absorption profile :param layers: indices of the layers in which to calculate the absorption profile. Layer 0 is the incidence medium. :return: A dictionary with the R, A and T at the specified wavelengths and angle. """ if 'OptiStack' in str(type(stack)): stack.no_back_reflection = no_back_reflection else: if hasattr(stack, 'substrate'): substrate = stack.substrate else: substrate = None stack = OptiStack(stack, no_back_reflection=no_back_reflection, substrate=substrate) if not coherent: if coherency_list is not None: assert len(coherency_list) == stack.num_layers, \ 'Error: The coherency list must have as many elements (now {}) as the ' \ 'number of layers (now {}).'.format(len(coherency_list), stack.num_layers) if stack.no_back_reflection: coherency_list = ['i'] + coherency_list + ['i', 'i'] else: coherency_list = ['i'] + coherency_list + ['i'] else: raise Exception('Error: For incoherent or partly incoherent calculations you must supply the ' 'coherency_list parameter with as many elements as the number of layers in the ' 'structure') self.stack = stack self.coherent = coherent self.coherency_list = coherency_list
def solve_tmm(solar_cell, options): """ Calculates the reflection, transmission and absorption 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). Relevant options are 'wl' (the wavelengths, in m), the incidence angle 'theta' (in degrees), the polarization 'pol' ('s', 'p' or 'u'), 'position' (locations in m at which depth-dependent absorption is calculated), 'no_back_reflexion' and 'BL_correction'. 'no_back_reflexion' sets whether reflections from the back surface are suppressed (if set to True, the default), or taken into account (if set to False). If 'BL_correction' 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. A coherency_list option can be provided: this should have elements equal to the total 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. :param solar_cell: A SolarCell object :param options: Options for the solver :return: None """ wl = options.wavelength BL_correction = options.BL_correction if 'BL_correction' in options.keys( ) else True theta = options.theta if 'theta' in options.keys( ) else 0 # angle IN DEGREES pol = options.pol if 'pol' in options.keys() else 'u' # 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 no_back_reflexion = options.no_back_reflexion if 'no_back_reflexion' in options.keys( ) else True full_stack = OptiStack(all_layers, no_back_reflexion=no_back_reflexion, substrate=solar_cell.substrate) if 'coherency_list' in options.keys(): coherency_list = options.coherency_list coherent = False assert len(coherency_list) == full_stack.num_layers, \ '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: coherency_list = None coherent = True if BL_correction and any( widths > 10 * np.max(wl)): # assume it's safe to ignore interference effects make_incoherent = np.where(np.array(widths) > 10 * np.max(wl))[0] print('Treating layer(s) ' + str(make_incoherent).strip('[]') + ' incoherently') if not 'coherency_list' in options.keys(): 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 = options.position * 1e9 profile_position = position[position < sum(full_stack.widths)] print('Calculating RAT...') RAT = calculate_rat(full_stack, wl * 1e9, angle=theta, coherent=coherent, coherency_list=coherency_list, no_back_reflexion=no_back_reflexion, pol=pol) print('Calculating absorption profile...') out = calculate_absorption_profile(full_stack, wl * 1e9, dist=profile_position, angle=theta, no_back_reflexion=no_back_reflexion, pol=pol, coherent=coherent, coherency_list=coherency_list) # 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. layer = 0 A_per_layer = np.array( RAT['A_per_layer'][1:-1]) # first entry is R, last entry is T for j in range(len(solar_cell)): solar_cell[j].diff_absorption = diff_absorption solar_cell[j].absorbed = types.MethodType(absorbed, solar_cell[j]) solar_cell[j].layer_absorption = initial * np.sum( A_per_layer[layer:(layer + n_layers_junction[j])], axis=0) 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
def initialise_S(stack, size, orders): S = S4.New(((size[0], 0), (0, size[1])), orders) geom_list = [layer.geometry for layer in stack] geom_list.insert(0, {}) # incidence medium geom_list.append({}) # transmission medium ## Materials for the shapes need to be defined before you can do .SetRegion shape_mats = necessary_materials(geom_list) Layers = [] for x in shape_mats: Layers.append(Layer(0, x)) shape_mats_OS = OptiStack(Layers) for i1 in range(len(shape_mats) ): # create the materials needed for all the shapes in S4 S.SetMaterial('shape_mat_' + str(i1 + 1), 1) ## Make the layers stack_OS = OptiStack(stack) widths = stack_OS.get_widths() for i1 in range( len(widths) ): # create 'dummy' materials for base layers including incidence and transmission media S.SetMaterial( 'layer_' + str(i1 + 1), 1 ) # This is not strictly necessary but it means S.SetExcitationPlanewave # can be done outside the wavelength loop in calculate_rat_rcwa for i1 in range(len(widths)): # set base layers layer_name = 'layer_' + str(i1 + 1) if widths[i1] == float('Inf'): S.AddLayer( layer_name, 0, layer_name ) # Solcore4 has incidence and transmission media widths set to Inf; # in S4 they have zero width else: S.AddLayer(layer_name, widths[i1], layer_name) # keep base unit m, not nm geometry = geom_list[i1] if bool(geometry): for shape in geometry: mat_name = 'shape_mat_' + str( shape_mats.index(shape['mat']) + 1) if shape['type'] == 'circle': S.SetRegionCircle(layer_name, mat_name, shape['center'], shape['radius']) elif shape['type'] == 'ellipse': S.SetRegionEllipse(layer_name, mat_name, shape['center'], shape['angle'], shape['halfwidths']) elif shape['type'] == 'rectangle': S.SetRegionRectangle(layer_name, mat_name, shape['center'], shape['angle'], shape['halfwidths']) elif shape['type'] == 'polygon': S.SetRegionPolygon(layer_name, mat_name, shape['center'], shape['angle'], shape['vertices']) return S, stack_OS, shape_mats_OS
def calculate_absorption_profile_rcwa(structure, size, orders, wavelength, rat_output, z_limit=None, steps_size=2, dist=None, theta=0, phi=0, pol='u'): """ It calculates the absorbed energy density within the material. From the documentation: 'In principle this has units of [power]/[volume], but we can express it as a multiple of incoming light power density on the material, which has units [power]/[area], so that absorbed energy density has units of 1/[length].' Integrating this absorption profile in the whole stack gives the same result that the absorption obtained with calculate_rat as long as the spacial mesh (controlled by steps_thinest_layer) is fine enough. If the structure is very thick and the mesh not thin enough, the calculation might diverege at short wavelengths. For now, it only works for normal incident, coherent light. :param structure: A solcore structure with layers and materials. :param wavelength: Wavelengths in which calculate the data (in nm). An array-like object. :return: A dictionary containing the positions (in nm) and a 2D array with the absorption in the structure as a function of the position and the wavelength. """ num_wl = len(wavelength) if dist is None: if z_limit is None: stack = OptiStack(structure) z_limit = np.sum(np.array(stack.widths)) dist = np.arange(0, z_limit, steps_size) output = {'position': dist, 'absorption': np.zeros((num_wl, len(dist)))} S, stack_OS, shape_mats_OS = initialise_S(structure, size, orders) if pol in 'sp': if pol == 's': s = 1 p = 0 elif pol == 'p': s = 0 p = 1 S.SetExcitationPlanewave((theta, phi), s, p, 0) for i, wl in enumerate(wavelength): update_epsilon(S, stack_OS, shape_mats_OS, wl) S.SetFrequency(1 / wl) A = rat_output['A'][i] for j, d in enumerate(dist): layer, d_in_layer = tmm.find_in_structure_with_inf( stack_OS.get_widths(), d) # don't need to change this layer_name = 'layer_' + str( layer + 1) # layer_1 is air above so need to add 1 data = rcwa_position_resolved(S, layer_name, d_in_layer, A) output['absorption'][i, j] = data else: for i, wl in enumerate( wavelength): # set the material values and indices in here update_epsilon(S, stack_OS, shape_mats_OS, wl) S.SetFrequency(1 / wl) A = rat_output['A'][i] for j, d in enumerate(dist): layer, d_in_layer = tmm.find_in_structure_with_inf( stack_OS.get_widths(), d) # don't need to change this layer_name = 'layer_' + str( layer + 1) # layer_1 is air above so need to add 1 S.SetExcitationPlanewave((theta, phi), 0, 1, 0) # p-polarization data_p = rcwa_position_resolved(S, layer_name, d_in_layer, A) S.SetExcitationPlanewave((theta, phi), 1, 0, 0) # p-polarization data_s = rcwa_position_resolved(S, layer_name, d_in_layer, A) output['absorption'][i, j] = 0.5 * (data_p + data_s) return output
def calculate_absorption_profile_rcwa(structure, size, orders, wavelength, rat_output_A, z_limit=None, steps_size=2, dist=None, theta=0, phi=0, pol='u', incidence=None, substrate=None, parallel=False, n_jobs=-1, user_options=None): """It calculates the absorbed energy density within the material. Integrating this absorption profile in the whole stack gives the same result as the absorption obtained with calculate_rat as long as the spatial mesh is fine enough. If the structure is very thick and the mesh not thin enough, the calculation might diverge at short wavelengths. This function is analogous to calculate_absorption_profile from the transfer_matrix module. :param structure: A Solcore structure with layers and materials. :param size: a tuple of 2-D vectors in the format ((ux, uy), (vx, vy)) giving the x and y components of the lattice unit vectors in nm. :param orders: number of orders to retain in the RCWA calculations. :param wavelength: Wavelengths (in nm) in which calculate the data. :param rat_output: A_pol' (polarization-dependent layer absorption) output from calculate_rat_rcwa :param z_limit: Maximum value in the z direction at which to calculate depth-dependent absorption (nm) :param steps_size: if the dist is not specified, the step size in nm to use in the depth-dependent calculation :param dist: the positions (in nm) at which to calculate depth-dependent absorption :param theta: polar incidence angle (in degrees) of the incident light. Default: 0 (normal incidence) :param phi: azimuthal incidence angle in degrees. Default: 0 :param pol: Polarisation of the light: 's', 'p' or 'u'. Default: 'u' (unpolarised). :param substrate: semi-infinite transmission medium :return: A dictionary containing the positions (in nm) and a 2D array with the absorption in the structure as a function of the position and the wavelength. """ num_wl = len(wavelength) geom_list = [layer.geometry for layer in structure] geom_list.insert(0, {}) # incidence medium geom_list.append({}) # transmission medium # write a separate function that makes the OptiStack structure into an S4 object, defined materials etc. # write a separate function that makes the OptiStack structure into an S4 object, defined materials etc. ## Materials for the shapes need to be defined before you can do .SetRegion shape_mats, geom_list_str = necessary_materials(geom_list) shapes_oc = np.zeros((len(wavelength), len(shape_mats)), dtype=complex) for i1, x in enumerate(shape_mats): shapes_oc[:, i1] = (x.n(wavelength / 1e9) + 1j * x.k(wavelength / 1e9))**2 stack_OS = OptiStack(structure, bo_back_reflection=False, substrate=substrate) widths = stack_OS.get_widths() layers_oc = np.zeros((len(wavelength / 1e9), len(structure) + 2), dtype=complex) if incidence == None: layers_oc[:, 0] = 1 else: layers_oc[:, 0] = (incidence.n( wavelength / 1e9))**2 # + 1j*incidence.k(wavelengths))**2 if substrate == None: layers_oc[:, -1] = 1 else: layers_oc[:, -1] = (substrate.n(wavelength / 1e9) + 1j * substrate.k(wavelength / 1e9))**2 for i1, x in enumerate(structure): layers_oc[:, i1 + 1] = (x.material.n(wavelength / 1e9) + 1j * x.material.k(wavelength / 1e9))**2 shapes_names = [str(x) for x in shape_mats] rcwa_options = DEFAULT_OPTIONS if user_options is not None: rcwa_options.update(user_options) if dist is None: if z_limit is None: stack = OptiStack(structure) z_limit = np.sum(np.array(stack.widths)) dist = np.arange(0, z_limit, steps_size) output = {'position': dist, 'absorption': np.zeros((num_wl, len(dist)))} if parallel: allres = Parallel(n_jobs=n_jobs)(delayed(RCWA_wl_prof)( wavelength[i1], rat_output_A[i1], dist, geom_list_str, layers_oc[i1], shapes_oc[i1], shapes_names, pol, theta, phi, widths, size, orders, rcwa_options) for i1 in range(num_wl)) else: allres = [ RCWA_wl_prof(wavelength[i1], rat_output_A[i1], dist, geom_list_str, layers_oc[i1], shapes_oc[i1], shapes_names, pol, theta, phi, widths, size, orders, rcwa_options) for i1 in range(num_wl) ] profile = np.stack(allres) output['absorption'] = profile return output
def calculate_rat_rcwa(structure, size, orders, wavelength, incidence, substrate, theta=0, phi=0, pol='u', parallel=False, n_jobs=-1, user_options=None): """Calculates the reflected, absorbed and transmitted intensity of the structure for the wavelengths and angles defined using an RCWA method implemented using the S4 package. This function is analogous to calculate_rat from the transfer_matrix module. :param structure: A Solcore Structure object with layers and materials or a OptiStack object. :param size: a tuple of 2-D vectors in the format ((ux, uy), (vx, vy)) giving the x and y components of the lattice unit vectors in nm. :param orders: number of orders to retain in the RCWA calculations. :param wavelength: Wavelengths (in nm) in which calculate the data. :param incidence: a Solcore material describing the semi-infinite incidence medium :param substrate: a Solcore material describing the semi-infinite transmission medium :param theta: polar incidence angle (in degrees) of the incident light. Default: 0 (normal incidence) :param phi: azimuthal incidence angle (in degrees). Default: 0 :param pol: Polarisation of the light: 's', 'p' or 'u'. Default: 'u' (unpolarised). :param parallel: whether or not to execute calculation in parallel (over wavelengths), True or False. Default is False :param n_jobs: the 'n_jobs' argument passed to Parallel from the joblib package. If set to -1, all available CPUs are used, if set to 1 no parallel computing is executed. The number of CPUs used is given by n_cpus + 1 + n_jobs. Default is -1. :param user_options: dictionary of options for S4. The list of possible entries and their values is: * LatticeTruncation: 'Circular' or 'Parallelogramic' (default 'Circular') * DiscretizedEpsilon: True or False (default False) * DiscretizationResolution: integer (default value 8) * PolarizationDecomposition: True or False (default False) * PolarizationBasis: 'Default' or 'Normal' or 'Jones' (default 'Default') * LanczosSmoothing: True or False (default False) * SubpixelSmoothing: True or False (default False) * ConserveMemory: True or False (default False) * WeismannFormulation: True or False (default False) Further information on the function of these options can be found in the S4 Python API documentation. If no options are provided, those from DEFAULT_OPTIONS are used. :return: A dictionary with the R, total A and T at the specified wavelengths and angle. A_pol lists total absorption at s and p polarizations if pol = 'u' was specified (this information is needed for the absorption profile calculation). Otherwise, A_pol is the same as A. """ num_wl = len(wavelength) # write a separate function that makes the OptiStack structure into an S4 object, defined materials etc. # Materials for the shapes need to be defined before you can do .SetRegion geom_list = [layer.geometry for layer in structure] geom_list.insert(0, {}) # incidence medium geom_list.append({}) # transmission medium shape_mats, geom_list_str = necessary_materials(geom_list) shapes_oc = np.zeros((len(wavelength), len(shape_mats)), dtype=complex) for i1, x in enumerate(shape_mats): shapes_oc[:, i1] = (x.n(wavelength / 1e9) + 1j * x.k(wavelength / 1e9))**2 stack_OS = OptiStack(structure, bo_back_reflection=False, substrate=substrate) widths = stack_OS.get_widths() layers_oc = np.zeros((len(wavelength / 1e9), len(structure) + 2), dtype=complex) if incidence == None: layers_oc[:, 0] = 1 else: layers_oc[:, 0] = (incidence.n( wavelength / 1e9))**2 # + 1j*incidence.k(wavelengths))**2 if substrate == None: layers_oc[:, -1] = 1 else: layers_oc[:, -1] = (substrate.n(wavelength / 1e9) + 1j * substrate.k(wavelength / 1e9))**2 for i1, x in enumerate(structure): layers_oc[:, i1 + 1] = (x.material.n(wavelength / 1e9) + 1j * x.material.k(wavelength / 1e9))**2 shapes_names = [str(x) for x in shape_mats] rcwa_options = DEFAULT_OPTIONS if user_options is not None: rcwa_options.update(user_options) if parallel: allres = Parallel(n_jobs=n_jobs)(delayed(RCWA_wl)( wavelength[i1], geom_list_str, layers_oc[i1], shapes_oc[i1], shapes_names, pol, theta, phi, widths, size, orders, rcwa_options) for i1 in range(num_wl)) else: allres = [ RCWA_wl(wavelength[i1], geom_list_str, layers_oc[i1], shapes_oc[i1], shapes_names, pol, theta, phi, widths, size, orders, rcwa_options) for i1 in range(num_wl) ] R = np.stack([item[0] for item in allres]) T = np.stack([item[1] for item in allres]) A_mat = np.stack([item[2] for item in allres]) if pol == 'u': A_per_layer = np.mean(A_mat, axis=1) A_pol = np.sum(A_mat, 2) else: A_per_layer = A_mat A_pol = np.sum(A_per_layer, 1) output = { 'R': R, 'A': np.sum(A_per_layer, 1), 'T': T, 'A_per_layer': A_per_layer, 'A_pol': A_pol } return output
def calculate_absorption_profile_rcwa(structure, size, orders, wavelength, rat_output, z_limit=None, steps_size=2, dist=None, theta=0, phi=0, pol='u', substrate=None): """ It calculates the absorbed energy density within the material. From the documentation: 'In principle this has units of [power]/[volume], but we can express it as a multiple of incoming light power density on the material, which has units [power]/[area], so that absorbed energy density has units of 1/[length].' Integrating this absorption profile in the whole stack gives the same result that the absorption obtained with calculate_rat as long as the spacial mesh (controlled by steps_thinest_layer) is fine enough. If the structure is very thick and the mesh not thin enough, the calculation might diverege at short wavelengths. For now, it only works for normal incident, coherent light. :param structure: A solcore structure with layers and materials. :param size: list with 2 entries, size of the unit cell (right now, can only be rectangular :param orders: number of orders to retain in the RCWA calculations. :param wavelength: Wavelengths (in nm) in which calculate the data. :param rat_output: output from calculate_rat_rcwa :param z_limit: Maximum value in the z direction at which to calculate depth-dependent absorption (nm) :param steps_size: if the dist is not specified, the step size in nm to use in the depth-dependent calculation :param dist: the positions (in nm) at which to calculate depth-dependent absorption :param theta: polar incidence angle (in degrees) of the incident light. Default: 0 (normal incidence) :param phi: azimuthal incidence angle in degrees. Default: 0 :param pol: Polarisation of the light: 's', 'p' or 'u'. Default: 'u' (unpolarised). :param substrate: semi-infinite transmission medium :return: A dictionary containing the positions (in nm) and a 2D array with the absorption in the structure as a function of the position and the wavelength. """ num_wl = len(wavelength) if dist is None: if z_limit is None: stack = OptiStack(structure) z_limit = np.sum(np.array(stack.widths)) dist = np.arange(0, z_limit, steps_size) output = {'position': dist, 'absorption': np.zeros((num_wl, len(dist)))} S, stack_OS, shape_mats_OS = initialise_S(structure, size, orders, substrate) if pol in 'sp': if pol == 's': s = 1 p = 0 elif pol == 'p': s = 0 p = 1 S.SetExcitationPlanewave((theta, phi), s, p, 0) for i, wl in enumerate(wavelength): update_epsilon(S, stack_OS, shape_mats_OS, wl) S.SetFrequency(1 / wl) A = rat_output['A'][i] for j, d in enumerate(dist): layer, d_in_layer = tmm.find_in_structure_with_inf( stack_OS.get_widths(), d) # don't need to change this layer_name = 'layer_' + str( layer + 1) # layer_1 is air above so need to add 1 data = rcwa_position_resolved(S, layer_name, d_in_layer, A) output['absorption'][i, j] = data else: for i, wl in enumerate( wavelength): # set the material values and indices in here print(i) update_epsilon(S, stack_OS, shape_mats_OS, wl) S.SetFrequency(1 / wl) A = rat_output['A'][i] for j, d in enumerate(dist): layer, d_in_layer = tmm.find_in_structure_with_inf( stack_OS.get_widths(), d) # don't need to change this layer_name = 'layer_' + str( layer + 1) # layer_1 is air above so need to add 1 S.SetExcitationPlanewave((theta, phi), 0, 1, 0) # p-polarization data_p = rcwa_position_resolved(S, layer_name, d_in_layer, A) S.SetExcitationPlanewave((theta, phi), 1, 0, 0) # p-polarization data_s = rcwa_position_resolved(S, layer_name, d_in_layer, A) output['absorption'][i, j] = 0.5 * (data_p + data_s) return output
ax3.set_xlabel('Wavelength (nm)') ax3.set_ylabel('Reflection / Absorption') ax3.set_title('c)', loc = 'left') #plt.legend() #plt.show() from solcore.absorption_calculator import calculate_rat, OptiStack ## pure TMM all_layers = front_materials + [Layer(bulkthick, Ge)] + back_materials coh_list = len(front_materials)*['c'] + ['i'] + ['c'] OS_layers = OptiStack(all_layers, substrate=Ag, no_back_reflection=False) TMM_res = calculate_rat(OS_layers, wavelength=wavelengths*1e9, no_back_reflection=False, angle=options['theta_in']*180/np.pi, coherent=False, coherency_list=coh_list, pol=options['pol']) #ax4.subplot(224) ax4.plot(options['wavelengths']*1e9, TMM_res['R'], label='R') ax4.plot(options['wavelengths']*1e9, TMM_res['A_per_layer'][1] + TMM_res['A_per_layer'][2], label='ARC') ax4.plot(options['wavelengths']*1e9, TMM_res['A_per_layer'][3], label='InGaP') ax4.plot(options['wavelengths']*1e9, TMM_res['A_per_layer'][4], label='GaAs') ax4.plot(options['wavelengths']*1e9, TMM_res['A_per_layer'][5], label='SiGeSn') ax4.plot(options['wavelengths']*1e9, TMM_res['A_per_layer'][len(front_materials)+1], label='Ge') ax4.plot(options['wavelengths']*1e9, TMM_res['T'], label='T') ax4.set_xlabel('Wavelength (nm)')
# pure TMM comparison layers = [ Layer(60e-9, SiN), Layer(50E-9, GaInP), Layer(50e-9, GaAs), Layer(20e-6, Si), Layer(100E-9, GaInP), Layer(70e-9, GaAs) ] from solcore.absorption_calculator import OptiStack, calculate_rat optist = OptiStack(layers, incidence=Air, substrate=SiN, no_back_reflection=False) TMM_res = calculate_rat(optist, options['wavelengths'] * 1e9, angle=options['theta_in'] * 180 / np.pi, pol=options['pol'], coherent=False, coherency_list=['c', 'c', 'c', 'i', 'c', 'c'], no_back_reflection=False) plt.figure() plt.plot(options['wavelengths'] * 1e9, TMM_res['R'], label='R') plt.plot(options['wavelengths'] * 1e9, TMM_res['T'], label='T') plt.plot(options['wavelengths'] * 1e9, TMM_res['A_per_layer'][4], label='A_bulk (Si)')
def solve_tmm(solar_cell, options): """ Calculates the reflection, transmission and absorption of a solar cell object using the transfer matrix method :param solar_cell: :param options: :return: """ wl = options.wavelength # 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 widths = [] offset = 0 all_layers = [] 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) # For each junction, and layer within the junction, we get the absorption coefficient and the layer width. elif type(layer_object) in [TunnelJunction, Junction]: junction_width = 0 try: for i, layer in enumerate(layer_object): all_layers.append(layer) junction_width += layer.width widths.append(layer.width) solar_cell[j].width = junction_width except TypeError as err: print( 'ERROR in "solar_cell_solver: TMM solver":\n' '\tNo layers found in Junction or TunnelJunction objects.') raise err solar_cell[j].offset = offset offset += layer_object.width # With all the information, we create the optical stack no_back_reflexion = options.no_back_reflexion if 'no_back_reflexion' in options.keys( ) else True stack = OptiStack(all_layers, no_back_reflexion=no_back_reflexion) dist = np.logspace(0, np.log10(offset * 1e9), int(300 * np.log10(offset * 1e9))) position = options.position if 'position' in options.keys() else dist print('Calculating RAT...') RAT = calculate_rat(stack, wl * 1e9, coherent=True) print('Calculating absorption profile...') out = calculate_absorption_profile(stack, wl * 1e9, dist=position) # With all this information, we are ready to calculate the differential absorption function diff_absorption, all_absorbed = calculate_absorption_tmm(out) # Each building block (layer or junction) needs to have access to the absorbed light in its region. # We update each object with that information. for j in range(len(solar_cell)): solar_cell[j].diff_absorption = diff_absorption solar_cell[j].absorbed = types.MethodType(absorbed, solar_cell[j]) solar_cell.reflected = RAT['R'] * initial solar_cell.transmitted = (1 - RAT['R'] - all_absorbed) * initial solar_cell.absorbed = all_absorbed * initial
def __init__(self, structure, size, orders, options, incidence, substrate): """ Calculates the reflected, absorbed and transmitted intensity of the structure for the wavelengths and angles defined using an RCWA method implemented using the S4 package. :param structure: A solcore Structure object with layers and materials or a OptiStack object. :param size: list with 2 entries, size of the unit cell (right now, can only be rectangular :param orders: number of orders to retain in the RCWA calculations. :param substrate: semi-infinite transmission medium :return: A dictionary with the R, A and T at the specified wavelengths and angle. """ wavelengths = options['wavelengths'] # write a separate function that makes the OptiStack structure into an S4 object, defined materials etc. geom_list = [layer.geometry for layer in structure] geom_list.insert(0, {}) # incidence medium geom_list.append({}) # transmission medium ## Materials for the shapes need to be defined before you can do .SetRegion shape_mats, geom_list_str = necessary_materials(geom_list) shapes_oc = np.zeros((len(wavelengths), len(shape_mats)), dtype=complex) for i1, x in enumerate(shape_mats): shapes_oc[:, i1] = (x.n(wavelengths) + 1j * x.k(wavelengths))**2 stack_OS = OptiStack(structure, bo_back_reflection=False, substrate=substrate) widths = stack_OS.get_widths() layers_oc = np.zeros((len(wavelengths), len(structure) + 2), dtype=complex) layers_oc[:, 0] = ( incidence.n(wavelengths))**2 # + 1j*incidence.k(wavelengths))**2 layers_oc[:, -1] = (substrate.n(wavelengths) + 1j * substrate.k(wavelengths))**2 for i1, x in enumerate(structure): layers_oc[:, i1 + 1] = (x.material.n(wavelengths) + 1j * x.material.k(wavelengths))**2 shapes_names = [str(x) for x in shape_mats] # nm_spacing = options['nm_spacing'] # RCWA options rcwa_options = dict(LatticeTruncation='Circular', DiscretizedEpsilon=False, DiscretizationResolution=8, PolarizationDecomposition=False, PolarizationBasis='Default', LanczosSmoothing=False, SubpixelSmoothing=False, ConserveMemory=False, WeismannFormulation=False) user_options = options[ 'rcwa_options'] if 'rcwa_options' in options.keys() else {} rcwa_options.update(user_options) self.wavelengths = wavelengths self.rcwa_options = rcwa_options self.options = options self.geom_list = geom_list_str self.shapes_oc = shapes_oc self.shapes_names = shapes_names self.widths = widths self.orders = orders self.size = size self.layers_oc = layers_oc
def RCWA(structure, size, orders, options, incidence, transmission, only_incidence_angle=False, front_or_rear='front', surf_name='', detail_layer=False, save=True): """ Calculates the reflected, absorbed and transmitted intensity of the structure for the wavelengths and angles defined using an RCWA method implemented using the S4 package. :param structure: A solcore Structure object with layers and materials or a OptiStack object. :param size: list with 2 entries, size of the unit cell (right now, can only be rectangular :param orders: number of orders to retain in the RCWA calculations. :param wavelength: Wavelengths (in nm) in which calculate the data. :param theta: polar incidence angle (in degrees) of the incident light. Default: 0 (normal incidence) :param phi: azimuthal incidence angle in degrees. Default: 0 :param pol: Polarisation of the light: 's', 'p' or 'u'. Default: 'u' (unpolarised). :param transmission: semi-infinite transmission medium :return: A dictionary with the R, A and T at the specified wavelengths and angle. """ # TODO: when non-zero incidence angle, not binned correctly in matrix (just goes in theta = 0) # TODO: when doing unpolarized, why not just set s=0.5 p=0.5 in S4? (Maybe needs to be normalised differently). Also don't know if this is faster, # or if internally it will still do s & p separately # TODO: if incidence angle is zero, s and p polarization are the same so no need to do both structpath = os.path.join(results_path, options['project_name']) if not os.path.isdir(structpath): os.mkdir(structpath) savepath_RT = os.path.join(structpath, surf_name + front_or_rear + 'RT.npz') savepath_A = os.path.join(structpath, surf_name + front_or_rear + 'A.npz') prof_mat_path = os.path.join(results_path, options['project_name'], surf_name + front_or_rear + 'profmat.nc') if os.path.isfile(savepath_RT) and save: print('Existing angular redistribution matrices found') full_mat = load_npz(savepath_RT) A_mat = load_npz(savepath_A) else: wavelengths = options['wavelengths'] if front_or_rear == 'front': layers = structure trns = transmission inc = incidence else: layers = structure[::-1] trns = incidence inc = transmission # write a separate function that makes the OptiStack structure into an S4 object, defined materials etc. geom_list = [layer.geometry for layer in structure] geom_list.insert(0, {}) # incidence medium geom_list.append({}) # transmission medium ## Materials for the shapes need to be defined before you can do .SetRegion shape_mats, geom_list_str = necessary_materials(geom_list) shapes_oc = np.zeros((len(wavelengths), len(shape_mats)), dtype=complex) for i1, x in enumerate(shape_mats): shapes_oc[:, i1] = (x.n(wavelengths) + 1j * x.k(wavelengths))**2 stack_OS = OptiStack(layers, no_back_reflection=False) widths = stack_OS.get_widths() layers_oc = np.zeros((len(wavelengths), len(structure) + 2), dtype=complex) layers_oc[:, 0] = (inc.n(wavelengths))**2 #+ 1j*inc.k(wavelengths))**2 layers_oc[:, -1] = (trns.n(wavelengths) + 1j * trns.k(wavelengths))**2 for i1, x in enumerate(layers): layers_oc[:, i1 + 1] = (x.material.n(wavelengths) + 1j * x.material.k(wavelengths))**2 shapes_names = [str(x) for x in shape_mats] #nm_spacing = options['nm_spacing'] phi_sym = options['phi_symmetry'] n_theta_bins = options['n_theta_bins'] c_az = options['c_azimuth'] pol = options['pol'] # RCWA options rcwa_options = dict(LatticeTruncation='Circular', DiscretizedEpsilon=False, DiscretizationResolution=8, PolarizationDecomposition=False, PolarizationBasis='Default', LanczosSmoothing=False, SubpixelSmoothing=False, ConserveMemory=False, WeismannFormulation=False) user_options = options[ 'rcwa_options'] if 'rcwa_options' in options.keys() else {} rcwa_options.update(user_options) print(rcwa_options) theta_intv, phi_intv, angle_vector = make_angle_vector( n_theta_bins, phi_sym, c_az) if only_incidence_angle: thetas_in = np.array([options['theta_in']]) phis_in = np.array([options['phi_in']]) else: angles_in = angle_vector[:int(len(angle_vector) / 2), :] thetas_in = angles_in[:, 1] phis_in = angles_in[:, 2] # angle in degrees thetas_in = thetas_in * 180 / np.pi phis_in = phis_in * 180 / np.pi # initialise_S has to happen inside parallel job (get Pickle errors otherwise); # just pass relevant optical constants for each wavelength, like for RT angle_vector_0 = angle_vector[:, 0] if front_or_rear == "front": side = 1 else: side = -1 if options['parallel']: allres = Parallel(n_jobs=options['n_jobs'])(delayed(RCWA_wl)( wavelengths[i1] * 1e9, geom_list, layers_oc[i1], shapes_oc[i1], shapes_names, pol, thetas_in, phis_in, widths, size, orders, phi_sym, theta_intv, phi_intv, angle_vector_0, rcwa_options, detail_layer, side) for i1 in range(len(wavelengths))) else: allres = [ RCWA_wl(wavelengths[i1] * 1e9, geom_list, layers_oc[i1], shapes_oc[i1], shapes_names, pol, thetas_in, phis_in, widths, size, orders, phi_sym, theta_intv, phi_intv, angle_vector_0, rcwa_options, detail_layer, side) for i1 in range(len(wavelengths)) ] R = np.stack([item[0] for item in allres]) T = np.stack([item[1] for item in allres]) A_mat = np.stack([item[2] for item in allres]) full_mat = stack([item[3] for item in allres]) int_mat = stack([item[4] for item in allres]) #T_mat = np.stack([item[4] for item in allres]) #full_mat = np.hstack((R_mat, T_mat)) #full_mat = COO(full_mat) A_mat = COO(A_mat) if save: save_npz(savepath_RT, full_mat) save_npz(savepath_A, A_mat) #R_pfbo = np.stack([item[3] for item in allres]) #T_pfbo = np.stack([item[4] for item in allres]) #phi_rt = np.stack([item[5] for item in allres]) #theta_r = np.stack([item[6] for item in allres]) #theta_t = np.stack([item[7] for item in allres]) #R_pfbo_2 = np.stack([item[8] for item in allres]) #return {'R': R, 'T':T, 'A_layer': A_mat, 'full_mat': full_mat, 'int_mat': int_mat}#'R_pfbo': R_pfbo, 'T_pfbo': T_pfbo, 'phi_rt': phi_rt, 'theta_r': theta_r, 'theta_t': theta_t}#, 'R_pfbo_2': R_pfbo_2} return full_mat, A_mat # , R, T
angle_degrees_in = 8 wavelengths = np.linspace(900, 1200, 30) sim_fig6 = np.loadtxt('data/optos_fig6_sim.csv', delimiter=',') sim_fig7 = np.loadtxt('data/optos_fig7_sim.csv', delimiter=',') sim_fig8 = np.loadtxt('data/optos_fig8_sim.csv', delimiter=',') rayflare_fig6 = np.loadtxt('fig6_rayflare.txt') rayflare_fig7 = np.loadtxt('fig7_rayflare.txt') rayflare_fig8 = np.loadtxt('fig8_rayflare.txt') # planar Si = material('Si_OPTOS')() Air = material('Air')() struc = OptiStack([Layer(si('200um'), Si)], substrate=Air) RAT = calculate_rat(struc, wavelength=wavelengths, coherent=True) fig = plt.figure() plt.plot(sim_fig6[:,0], sim_fig6[:,1], '--', color=palhf[0], label= 'OPTOS - rear grating (a)') plt.plot(wavelengths, rayflare_fig6, '-o', color=palhf[0], label='RayFlare - rear grating (a)', fillstyle='none') plt.plot(sim_fig7[:,0], sim_fig7[:,1], '--', color=palhf[1], label= 'OPTOS - front pyramids (b)') plt.plot(wavelengths, rayflare_fig7, '-o', color=palhf[1], label= 'RayFlare - front pyramids (b)', fillstyle='none') plt.plot(sim_fig8[:,0], sim_fig8[:,1], '--', color=palhf[2], label= 'OPTOS - grating + pyramids (c)') plt.plot(wavelengths, rayflare_fig8, '-o', color=palhf[2], label= 'RayFlare - grating + pyramids (c)', fillstyle='none') plt.plot(wavelengths, RAT['A_per_layer'][1], '-k', label='Planar') plt.legend(loc='lower left') plt.xlabel('Wavelength (nm)') plt.ylabel('Absorption in Si') plt.xlim([900, 1200])
def make_TMM_lookuptable(layers, incidence, transmission, surf_name, options, coherent=True, coherency_list=None, prof_layers=None, sides=[1, -1]): structpath = os.path.join(results_path, options['project_name']) if not os.path.isdir(structpath): os.mkdir(structpath) savepath = os.path.join(structpath, surf_name + '.nc') if os.path.isfile(savepath): print('Existing lookup table found') allres = xr.open_dataset(savepath) else: wavelengths = options['wavelengths'] * 1e9 # convert to nm #pol = options['pol'] n_angles = options['lookuptable_angles'] thetas = np.linspace(0, np.pi / 2, n_angles) if prof_layers is not None: profile = True else: profile = False n_layers = len(layers) optlayers = OptiStack(layers, substrate=transmission, incidence=incidence) optlayers_flip = OptiStack(layers[::-1], substrate=incidence, incidence=transmission) optstacks = [optlayers, optlayers_flip] if coherency_list is not None: coherency_lists = [coherency_list, coherency_list[::-1]] else: coherency_lists = [['c'] * n_layers] * 2 # can calculate by angle, already vectorized over wavelength pols = ['s', 'p'] R = xr.DataArray(np.empty((2, 2, len(wavelengths), n_angles)), dims=['side', 'pol', 'wl', 'angle'], coords={ 'side': sides, 'pol': pols, 'wl': wavelengths, 'angle': thetas }, name='R') T = xr.DataArray(np.empty((2, 2, len(wavelengths), n_angles)), dims=['side', 'pol', 'wl', 'angle'], coords={ 'side': sides, 'pol': pols, 'wl': wavelengths, 'angle': thetas }, name='T') Alayer = xr.DataArray(np.empty( (2, 2, n_angles, len(wavelengths), n_layers)), dims=['side', 'pol', 'angle', 'wl', 'layer'], coords={ 'side': sides, 'pol': pols, 'wl': wavelengths, 'angle': thetas, 'layer': range(1, n_layers + 1) }, name='Alayer') if profile: Aprof = xr.DataArray( np.empty( (2, 2, n_angles, 6, len(prof_layers), len(wavelengths))), dims=['side', 'pol', 'angle', 'coeff', 'layer', 'wl'], coords={ 'side': sides, 'pol': pols, 'wl': wavelengths, 'angle': thetas, 'layer': prof_layers, 'coeff': ['A1', 'A2', 'A3_r', 'A3_i', 'a1', 'a3'] }, name='Aprof') for i1, side in enumerate(sides): R_loop = np.empty((len(wavelengths), n_angles)) T_loop = np.empty((len(wavelengths), n_angles)) Alayer_loop = np.empty((n_angles, len(wavelengths), n_layers), dtype=np.complex_) if profile: Aprof_loop = np.empty( (n_angles, 6, len(prof_layers), len(wavelengths))) for i2, pol in enumerate(pols): for i3, theta in enumerate(thetas): tmm_struct = tmm_structure( optstacks[i1], coherent=coherent, coherency_list=coherency_lists[i1]) #print(side, pol, theta) res = tmm_struct.calculate(wavelengths, angle=theta, pol=pol, profile=profile, layers=prof_layers, nm_spacing=1e5) R_loop[:, i3] = np.real(res['R']) T_loop[:, i3] = np.real(res['T']) Alayer_loop[i3, :, :] = np.real(res['A_per_layer'].T) if profile: Aprof_loop[i3, :, :, :] = res['profile_coeff'] # sometimes get very small negative values (like -1e-20) R_loop[R_loop < 0] = 0 T_loop[T_loop < 0] = 0 Alayer_loop[Alayer_loop < 0] = 0 if side == -1: Alayer_loop = np.flip(Alayer_loop, axis=2) if profile: Aprof_loop = np.flip(Aprof_loop, axis=2) R.loc[dict(side=side, pol=pol)] = R_loop T.loc[dict(side=side, pol=pol)] = T_loop Alayer.loc[dict(side=side, pol=pol)] = Alayer_loop if profile: Aprof.loc[dict(side=side, pol=pol)] = Aprof_loop Aprof.transpose('side', 'pol', 'wl', 'angle', 'layer', 'coeff') Alayer = Alayer.transpose('side', 'pol', 'wl', 'angle', 'layer') if profile: allres = xr.merge([R, T, Alayer, Aprof]) else: allres = xr.merge([R, T, Alayer]) unpol = allres.reduce(np.mean, 'pol').assign_coords(pol='u').expand_dims('pol') allres = allres.merge(unpol) allres.to_netcdf(savepath) return allres
plt.figure() plt.plot(wavelengths * 1e9, results_front[2][:, angle_index], 'r-') plt.plot(wavelengths * 1e9, results_front[3][:, angle_index], 'r--') plt.plot(wavelengths * 1e9, results_back[2][:, -(angle_index + 1)], 'k-') plt.plot(wavelengths * 1e9, results_back[3][:, -(angle_index + 1)], 'k--') plt.show() inc_angle = angle_vector[angle_index, 1] * 180 / np.pi from solcore.absorption_calculator import OptiStack, calculate_rat layers = [Layer(500e-9, GaAs), Layer(200e-9, Ge)] OptSt = OptiStack(layers, no_back_reflection=False, substrate=Si, incidence=Air) RAT = calculate_rat(OptSt, options['wavelengths'] * 1e9, angle=inc_angle, no_back_reflection=False, pol=options['pol']) Ge_e = np.real((Ge.n(wavelengths) + 1j * Ge.k(wavelengths))**2) GaAs_e = np.real((GaAs.n(wavelengths) + 1j * GaAs.k(wavelengths))**2) plt.figure() plt.plot(wavelengths * 1e9, RAT['A_per_layer'][1] / Af_1, label='A1 TMM/RCWA') plt.plot(wavelengths * 1e9, RAT['A_per_layer'][2] / Af_2, label='A2 TMM/RCWA') plt.plot(wavelengths * 1e9, RAT['R'] / Rf, label='R TMM/RCWA')
sio2_data = np.loadtxt(siO2_filename, skiprows=3, unpack=True) sio2_data[0] = 1.24 / sio2_data[0] n = np.sqrt(sio2_data[1] + sio2_data[2] * 1.j) sio2_data[1] = np.real(n) sio2_data[2] = np.imag(n) model_sio2 = [1.5, sio2_data[0] * 1000, sio2_data[1], sio2_data[2]] # gau1 = Gauss(A=2.8468, Ec=0.1299, Br=0.0111) # gau2 = Gauss(A=7.192, Ec=0.058331, Br=0.01682) # gau3 = Gauss(A=1.9737, Ec=0.13991, Br=0.02144) # cau = Cauchy(An=1.5, Bn=0.002, Ak=1, Bk=0.2580645161) # model_sio2 = [1.5, DielectricConstantModel(e_inf=3, oscillators=[cau])] stack = OptiStack([model_sio2, model_substrate]) # Fitting stuff fit = EllipsometryFitter(data, stack) variables = [['width'], ['e_inf', ['An']]] fit.set_variables(variables) wl = np.logspace(np.log10(300), np.log10(20000), 200) fit.set_range(wl) fit.plot() # fit.fit() # # print(fit.errors) # print(fit.correlations)
def TMM(layers, incidence, transmission, surf_name, options, coherent=True, coherency_list=None, prof_layers=[], front_or_rear='front', save=True): """Function which takes a layer stack and creates an angular redistribution matrix. :param layers: A list with one or more layers. :param transmission: transmission medium :param incidence: incidence medium :param surf_name: name of the surface (to save the matrices generated. :param options: a list of options :param coherent: whether or not the layer stack is coherent. If None, it is assumed to be fully coherent :param coherency: a list with the same number of entries as the layers, either 'c' for a coherent layer or 'i' for an incoherent layer :param prof_layers: layers for which the absorption profile should be calculated (if None, do not calculate absorption profile at all) :param front_or_rear: a string, either 'front' or 'rear'; front incidence on the stack, from the incidence medium, or rear incidence on the stack, from the transmission medium. :return full_mat: R and T redistribution matrix :return A_mat: matrix describing absorption per layer """ def make_matrix_wl(wl): # binning into matrix, including phi RT_mat = np.zeros((len(theta_bins_in)*2, len(theta_bins_in))) A_mat = np.zeros((n_layers, len(theta_bins_in))) for i1 in range(len(theta_bins_in)): theta = theta_lookup[i1]#angle_vector[i1, 1] data = allres.loc[dict(angle=theta, wl=wl)] R_prob = np.real(data['R'].data.item(0)) T_prob = np.real(data['T'].data.item(0)) Alayer_prob = np.real(data['Alayer'].data) phi_out = phis_out[i1] #print(R_prob, T_prob) # reflection phi_int = phi_intv[theta_bins_in[i1]] phi_ind = np.digitize(phi_out, phi_int, right=True) - 1 bin_out_r = np.argmin(abs(angle_vector[:, 0] - theta_bins_in[i1])) + phi_ind #print(bin_out_r, i1+offset) RT_mat[bin_out_r, i1] = R_prob #print(R_prob) # transmission theta_t = np.abs(-np.arcsin((inc.n(wl * 1e-9) / trns.n(wl * 1e-9)) * np.sin(theta_lookup[i1])) + quadrant) #print('angle in, transmitted', angle_vector_th[i1], theta_t) # theta switches half-plane (th < 90 -> th >90 if ~np.isnan(theta_t): theta_out_bin = np.digitize(theta_t, theta_intv, right=True) - 1 phi_int = phi_intv[theta_out_bin] phi_ind = np.digitize(phi_out, phi_int, right=True) - 1 bin_out_t = np.argmin(abs(angle_vector[:, 0] - theta_out_bin)) + phi_ind RT_mat[bin_out_t, i1] = T_prob #print(bin_out_t, i1+offset) # absorption A_mat[:, i1] = Alayer_prob fullmat = COO(RT_mat) A_mat = COO(A_mat) return fullmat, A_mat structpath = os.path.join(results_path, options['project_name']) if not os.path.isdir(structpath): os.mkdir(structpath) savepath_RT = os.path.join(structpath, surf_name + front_or_rear + 'RT.npz') savepath_A = os.path.join(structpath, surf_name + front_or_rear + 'A.npz') prof_mat_path = os.path.join(results_path, options['project_name'], surf_name + front_or_rear + 'profmat.nc') if os.path.isfile(savepath_RT) and save: print('Existing angular redistribution matrices found') fullmat = load_npz(savepath_RT) A_mat = load_npz(savepath_A) if len(prof_layers) > 0: profile = xr.load_dataarray(prof_mat_path) return fullmat, A_mat, profile else: wavelengths = options['wavelengths']*1e9 # convert to nm theta_intv, phi_intv, angle_vector = make_angle_vector(options['n_theta_bins'], options['phi_symmetry'], options['c_azimuth']) angles_in = angle_vector[:int(len(angle_vector) / 2), :] thetas = np.unique(angles_in[:, 1]) n_angles = len(thetas) n_layers = len(layers) if front_or_rear == 'front': optlayers = OptiStack(layers, substrate=transmission, incidence=incidence) trns = transmission inc = incidence else: optlayers = OptiStack(layers[::-1], substrate=incidence, incidence=transmission) trns = incidence inc = transmission if len(prof_layers) > 0: profile = True z_limit = np.sum(np.array(optlayers.widths)) full_dist = np.arange(0, z_limit, options['nm_spacing']) layer_start = np.insert(np.cumsum(np.insert(optlayers.widths, 0, 0)), 0, 0) layer_end = np.cumsum(np.insert(optlayers.widths, 0, 0)) dist = [] for l in prof_layers: dist = np.hstack((dist, full_dist[np.all((full_dist >= layer_start[l], full_dist < layer_end[l]), 0)])) else: profile = False if options['pol'] == 'u': pols = ['s', 'p'] else: pols = [options['pol']] R = xr.DataArray(np.empty((len(pols), len(wavelengths), n_angles)), dims=['pol', 'wl', 'angle'], coords={'pol': pols, 'wl': wavelengths, 'angle': thetas}, name='R') T = xr.DataArray(np.empty((len(pols), len(wavelengths), n_angles)), dims=['pol', 'wl', 'angle'], coords={'pol': pols, 'wl': wavelengths, 'angle': thetas}, name='T') Alayer = xr.DataArray(np.empty((len(pols), n_angles, len(wavelengths), n_layers)), dims=['pol', 'angle', 'wl', 'layer'], coords={'pol': pols, 'wl': wavelengths, 'angle': thetas, 'layer': range(1, n_layers + 1)}, name='Alayer') theta_t = xr.DataArray(np.empty((len(pols), len(wavelengths), n_angles)), dims=['pol', 'wl', 'angle'], coords={'pol': pols, 'wl': wavelengths, 'angle': thetas}, name='theta_t') if profile: Aprof = xr.DataArray(np.empty((len(pols), n_angles, len(wavelengths), len(dist))), dims=['pol', 'angle', 'wl', 'z'], coords={'pol': pols, 'wl': wavelengths, 'angle': thetas, 'z': dist}, name='Aprof') R_loop = np.empty((len(wavelengths), n_angles)) T_loop = np.empty((len(wavelengths), n_angles)) Alayer_loop = np.empty((n_angles, len(wavelengths), n_layers), dtype=np.complex_) th_t_loop = np.empty((len(wavelengths), n_angles)) if profile: Aprof_loop = np.empty((n_angles, len(wavelengths), len(dist))) tmm_struct = tmm_structure(optlayers, coherent=coherent, coherency_list=coherency_list, no_back_reflection=False) for i2, pol in enumerate(pols): for i3, theta in enumerate(thetas): res = tmm_struct.calculate(wavelengths, angle=theta, pol=pol, profile=profile, layers=prof_layers, nm_spacing = options['nm_spacing']) R_loop[:, i3] = np.real(res['R']) T_loop[:, i3] = np.real(res['T']) Alayer_loop[i3, :, :] = np.real(res['A_per_layer'].T) if profile: Aprof_loop[i3, :, :] = res['profile'] # sometimes get very small negative values (like -1e-20) R_loop[R_loop < 0] = 0 T_loop[T_loop < 0] = 0 Alayer_loop[Alayer_loop < 0] = 0 if front_or_rear == 'rear': Alayer_loop = np.flip(Alayer_loop, axis=2) print('flipping') R.loc[dict(pol=pol)] = R_loop T.loc[dict(pol=pol)] = T_loop Alayer.loc[dict(pol=pol)] = Alayer_loop theta_t.loc[dict(pol=pol)] = th_t_loop if profile: Aprof.loc[dict(pol=pol)] = Aprof_loop Aprof.transpose('pol', 'wl', 'angle', 'z') Alayer = Alayer.transpose('pol', 'wl', 'angle', 'layer') if profile: allres = xr.merge([R, T, Alayer, Aprof]) else: allres = xr.merge([R, T, Alayer]) if options['pol'] == 'u': allres = allres.reduce(np.mean, 'pol').assign_coords(pol='u').expand_dims('pol') # populate matrices if front_or_rear == "front": angle_vector_th = angle_vector[:int(len(angle_vector)/2),1] angle_vector_phi = angle_vector[:int(len(angle_vector)/2),2] phis_out = fold_phi(angle_vector_phi + np.pi, options['phi_symmetry']) theta_lookup = angles_in[:,1] quadrant = np.pi else: angle_vector_th = angle_vector[int(len(angle_vector) / 2):, 1] angle_vector_phi = angle_vector[int(len(angle_vector) / 2):, 2] phis_out = fold_phi(angle_vector_phi + np.pi, options['phi_symmetry']) theta_lookup = angles_in[:,1][::-1] quadrant = 0 phis_out[phis_out == 0] = 1e-10 theta_bins_in = np.digitize(angle_vector_th, theta_intv, right=True) -1 print(theta_bins_in) mats = [make_matrix_wl(wl) for wl in wavelengths] fullmat = stack([item[0] for item in mats]) A_mat = stack([item[1] for item in mats]) if save: save_npz(savepath_RT, fullmat) save_npz(savepath_A, A_mat) return fullmat, A_mat #, allres