import numpy as np
import os
import matplotlib.pyplot as plt
import seaborn as sns
from rayflare.ray_tracing import rt_structure
from rayflare.textures import regular_pyramids, planar_surface
from rayflare.options import default_options
from rayflare.utilities import make_absorption_function
from solcore.absorption_calculator import search_db
from solcore import material, si
from solcore.solar_cell import SolarCell, Layer, Junction
from solcore.solar_cell_solver import solar_cell_solver
from solcore.solar_cell_solver import default_options as defaults_solcore
Example 4a: Textured Si cell
In this example, we will introduce RayFlare, which is a package which is closely interlinked with Solcore and extends its optical capabilities. One of the features it has is a ray-tracer, which is useful when modelling e.g. Si solar cells with textured surfaces. We will compare the result with PVLighthouse’s wafer ray tracer.
For more information on how ray-tracing works, see RayFlare’s documentation.
Setting up
First, setting up Solcore materials. We use a specific set of Si optical constants from this paper. These are included in the refractiveindex.info database, so we take them from there. This is the same data we used for the PVLighthouse calculation which we are going to compare to.
= material('Air')()
Air = search_db(os.path.join("Si", "Green-2008"))[0][0]
Si_Green = material(str(Si_Green), nk_db=True)() Si_RT
Database file found at /Users/phoebe/.solcore/nk/nk.db
1 results found.
pageid shelf book page filepath hasrefractive hasextinction rangeMin rangeMax points
687 main Si Green-2008 main/Si/Green-2008.yml 1 1 0.25 1.45 121
The calc
variable is a switch: if True, run the calculation; if False, load the result of the previous calculation. Will need to run at least once to generate the results!
We use this ‘switch’ to avoid re-running the whole ray-tracing calculation (which can be time-consuming) each time we want to look at the results.
= True calc
Setting options:
= np.linspace(300, 1201, 50) * 1e-9
wl = default_options()
options = wl
options.wavelengths
# setting up some colours for plotting
= sns.color_palette("husl", 4) pal
nx
and ny
are the number of point to scan across in the x & y directions in the unit cell. Decrease this to speed up the calculation (but increase noise in results). We also set the total number of rays traced, and depth spacing for the absorption profile calculation.
= 25
nxy = nxy
options.nx = nxy
options.ny = 4 * nxy ** 2 # Number of rays to be traced at each wavelength:
options.n_rays = si('50nm') # depth spacing for the absorption profile
options.depth_spacing = True # this is the default - if you do not want the code to run in parallel, change to False options.parallel
Load the result of the PVLighthouse calculation for comparison:
= np.loadtxt(os.path.join("data", "RAT_data_300um_2um_55.csv"), delimiter=',', skiprows=1) PVlighthouse
Define surface for the ray-tracing: a planar surface, and a surface with regular pyramids.
= planar_surface(size=2) # pyramid size in microns
flat_surf = regular_pyramids(55, upright=False, size=2) triangle_surf
Set up the ray-tracing structure: this is a list of textures of length n, and then a list of materials of length n-1. So far a single layer, we define a front surface and a back surface (n = 2), and specify the material in between those two surfaces (n-1 = 1). We also specify the width of each material, and the incidence medium (above the first interface) and the transmission medium (below the last interface.
= rt_structure(textures=[triangle_surf, flat_surf],
rtstr = [Si_RT],
materials =[si('300um')], incidence=Air, transmission=Air) widths
Running ray-tracing calculation
Run the calculation, if calc
was set to True, otherwise load the results. We save the reflection, transmission, and total absorption in an array called result_RAT
and the absorption profile as profile_rt
.
if calc:
# This executes if calc = True (set at the top of the script): actually run the ray-tracing:
= rtstr.calculate_profile(options)
result
# Put the results (Reflection, front surface reflection, transmission, absorption in the Si) in an array:
= np.vstack((options['wavelengths']*1e9,
result_RAT 'R'], result['R0'], result['T'], result['A_per_layer'][:,0])).T
result[
# absorption profile:
= result['profile']
profile_rt
# save the results:
"results", "rayflare_fullrt_300um_2umpyramids_300_1200nm.txt"), result_RAT)
np.savetxt(os.path.join("results", "rayflare_fullrt_300um_2umpyramids_300_1200nm_profile.txt"), result['profile'])
np.savetxt(os.path.join(
else:
# If calc = False, load results from previous run.
= np.loadtxt(os.path.join("results", "rayflare_fullrt_300um_2umpyramids_300_1200nm.txt"))
result_RAT = np.loadtxt(os.path.join("results", "rayflare_fullrt_300um_2umpyramids_300_1200nm_profile.txt")) profile_rt
PLOT 1: results of ray-tracing from RayFlare and PVLighthouse, showing the reflection, absorption and transmission.
plt.figure()0], result_RAT[:,1], '-o', color=pal[0], label=r'R$_{total}$', fillstyle='none')
plt.plot(result_RAT[:,0], result_RAT[:,2], '-o', color=pal[1], label=r'R$_0$', fillstyle='none')
plt.plot(result_RAT[:,0], result_RAT[:,3], '-o', color=pal[2], label=r'T', fillstyle='none')
plt.plot(result_RAT[:,0], result_RAT[:,4], '-o', color=pal[3], label=r'A', fillstyle='none')
plt.plot(result_RAT[:,0], PVlighthouse[:, 2], '--', color=pal[0])
plt.plot(PVlighthouse[:, 0], PVlighthouse[:, 9], '--', color=pal[2])
plt.plot(PVlighthouse[:, 0], PVlighthouse[:, 3], '--', color=pal[1])
plt.plot(PVlighthouse[:, 0], PVlighthouse[:, 5], '--', color=pal[3])
plt.plot(PVlighthouse[:, -1, -1, '-ok', label='RayFlare')
plt.plot(-1, -1, '--k', label='PVLighthouse')
plt.plot('Wavelength (nm)')
plt.xlabel('R / A / T')
plt.ylabel(0, 1)
plt.ylim(300, 1200)
plt.xlim(
plt.legend()"(1) R/A/T for pyramid-textured Si, calculated with RayFlare and PVLighthouse")
plt.title( plt.show()
Using optical results in Solcore
So far, we have done a purely optical calculation; however, if we want to use this information to do an EQE or IV calculation, we can, by using the ability of Solcore to accept external optics data (we used this in Example 1a already). To use Solcore’s device simulation capabilities (QE/IV), we need to create a function which gives the depth-dependent absorption profile. The argument of the function is the position (in m) in the cell, which can be an array, and the function returns an array with the absorption at these depths at every wavelength with dimensions (n_wavelengths, n_positions).
RayFlare has the make_absorption_function to automatically make this function, as required by Solcore, from RayFlare’s output data. diff_absorb_fn
here is the function we need to pass to Solcore (so it is not an array of values!). We need to provide the profile data, the structure that was being simulated, user options and specify whether we used the angular redistribution matrix method (which in this case we did not, so we set matrix_method=False
; see [Example 6a]](6a-multiscale_models.ipynb) for a similar example which does use this method).
= make_absorption_function(profile_rt, rtstr, options, matrix_method=False) position, diff_absorb_fn
Now we feed this into Solcore; we will define a solar cell model using the depletion approximation (see Example 1c).
We need a p-n junction; we make sure the total width of the p-n junction is equal to the width of the Si used above in the ray-tracing calculation (rtrst.widths[0]
).
= material("Si")
Si_base
= si("500nm")
n_material_Si_width = rtstr.widths[0] - n_material_Si_width
p_material_Si_width
= Si_base(Nd=si(1e21, "cm-3"), hole_diffusion_length=si("10um"),
n_material_Si =50e-4, relative_permittivity=11.68)
electron_mobility= Si_base(Na=si(1e16, "cm-3"), electron_diffusion_length=si("290um"),
p_material_Si =400e-4, relative_permittivity=11.68) hole_mobility
Options for Solcore (note that these are separate from the RayFlare options we set above!):
= defaults_solcore
options_sc = "external"
options_sc.optics_method = np.arange(0, rtstr.width, options.depth_spacing)
options_sc.position = True
options_sc.light_iv = wl
options_sc.wavelength = options.theta_in*180/np.pi
options_sc.theta = np.linspace(0, 1, 200)
V = V options_sc.voltages
Make the solar cell, passing the absorption function we made above, and the reflection (an array with the R value at each wavelength), and calculate the QE and I-V characteristics.
= SolarCell(
solar_cell
[=n_material_Si_width, material=n_material_Si, role='emitter'),
Junction([Layer(width=p_material_Si_width, material=p_material_Si, role='base')],
Layer(width=1, sp=1, kind='DA')
sn
],=result_RAT[:,1],
external_reflected=diff_absorb_fn)
external_absorbed
'qe', options_sc)
solar_cell_solver(solar_cell, 'iv', options_sc) solar_cell_solver(solar_cell,
Solving optics of the solar cell...
Solving QE of the solar cell...
Solving optics of the solar cell...
Already calculated reflection, transmission and absorption profile - not recalculating. Set recalculate_absorption to True in the options if you want absorption to be calculated again.
Solving IV of the junctions...
Solving IV of the tunnel junctions...
Solving IV of the total solar cell...
PLOT 2: EQE and absorption of Si cell with optics calculated through ray-tracing
plt.figure()*1e9, solar_cell.absorbed, 'k-', label='Absorbed (integrated)')
plt.plot(wl*1e9, solar_cell[0].eqe(wl), 'r-', label='EQE')
plt.plot(wl*1e9, result_RAT[:,4], 'r--', label='Absorbed - RT')
plt.plot(wl0,1)
plt.ylim(
plt.legend()'Wavelength (nm)')
plt.xlabel('R/A')
plt.ylabel("(2) EQE/absorption from electrical model")
plt.title( plt.show()
PLOT 3: Light IV of Si cell with optics calculated through ray-tracing
plt.figure()-solar_cell[0].iv(V), 'r')
plt.plot(V, -20, 400)
plt.ylim(0, 0.8)
plt.xlim(
plt.legend()'Current (A/m$^2$)')
plt.ylabel('Voltage (V)')
plt.xlabel("(3) IV characteristics")
plt.title( plt.show()
No artists with labels found to put in legend. Note that artists whose label start with an underscore are ignored when legend() is called with no argument.