import numpy as np
import os
from solcore.structure import Layer
from solcore.constants import q
from solcore import material
from solcore.absorption_calculator import search_db, download_db
from solcore.light_source import LightSource
from rayflare.textures import regular_pyramids
from rayflare.options import default_options
from rayflare.ray_tracing import rt_structure
import matplotlib.pyplot as plt
import seaborn as sns
from cycler import cycler
Section 10: Perovskite-Si tandem cell with pyramidal surfaces
This example shows how you can simulate a perovskite-Si tandem cell with pyramidal surface textures, where the perovskite and other surface layers are assumed to be deposited conformally (i.e., also in a pyramid shape) on top of the Si. The perovskite optical constants are from this paper, while the structure is based on this paper We will calculate total reflection, transmission and absorption per layer as well as the wavelength-dependent absorption profiles in the perovskite and Si, which can be used in e.g. device simulations. We will look at the effect of treating the layers deposited on Si (including the perovskite) coherently or incoherently.
First, import relevant packages and RayFlare functions:
Now we set some relevant options. We will scan across 20 x 20 surface points of the pyramid unit cell between 300 and 1200 nm, for unpolarized, normally-incident light. The randomize_surface
option is set to True to prevent correlation between the incident position on the front and rear pyramids. The n_jobs
option is set to -1, which means that all available cores will be used. If you want to use all but one core, change this to -2 etc. We also need to provide a project_name
to save the lookup tables which will be calculated using TMM to use during ray-tracing.
= np.linspace(300, 1200, 40) * 1e-9
wavelengths
= LightSource(source_type="standard", version="AM1.5g", x=wavelengths,
AM15G ="photon_flux_per_m")
output_units
= default_options()
options = wavelengths
options.wavelengths = 20
options.nx = options.nx
options.ny = 4 * options.nx**2
options.n_rays = 1e-9
options.depth_spacing = "u"
options.pol = 1e-3
options.I_thresh = "perovskite_Si_rt"
options.project_name = True
options.randomize_surface = -1 # use all cores; to use all but one, change to -2 etc. options.n_jobs
We define our materials. Note that some of these are custom materials added to the database; we only need to do this once. We then define the front layer stack (i.e. all the materials which are on top of the Si, excluding Si itself, which will be the ‘bulk’ material) and the rear layer stack. Layer stacks are always defined starting with the layer closest to the top of the cell.
# Can comment out this block after running once to add materials to the database
from solcore.material_system import create_new_material
"Perovskite_CsBr_1p6eV", "data/CsBr10p_1to2_n_shifted.txt",
create_new_material("data/CsBr10p_1to2_k_shifted.txt")
"ITO_lowdoping", "data/model_back_ito_n.txt",
create_new_material("data/model_back_ito_k.txt")
"aSi_i", "data/model_i_a_silicon_n.txt",
create_new_material("data/model_i_a_silicon_k.txt")
"aSi_p", "data/model_p_a_silicon_n.txt",
create_new_material("data/model_p_a_silicon_k.txt")
"aSi_n", "data/model_n_a_silicon_n.txt",
create_new_material("data/model_n_a_silicon_k.txt")
"C60", "data/C60_Ren_n.txt",
create_new_material("data/C60_Ren_k.txt")
"IZO", "data/IZO_Ballif_rO2_10pcnt_n.txt",
create_new_material("data/IZO_Ballif_rO2_10pcnt_k.txt")
# Comment out until here
Material created with optical constants n and k only.
Material created with optical constants n and k only.
Material created with optical constants n and k only.
Material created with optical constants n and k only.
Material created with optical constants n and k only.
Material created with optical constants n and k only.
Material created with optical constants n and k only.
download_db()
= search_db(os.path.join("MgF2", "Rodriguez-de Marcos"))[0][0];
MgF2_pageid = search_db(os.path.join("Ag", "Jiang"))[0][0];
Ag_pageid
= material("Si")()
Si = material("Air")()
Air = material(str(MgF2_pageid), nk_db=True)()
MgF2 = material("ITO_lowdoping")()
ITO_back = material("Perovskite_CsBr_1p6eV")()
Perovskite = material(str(Ag_pageid), nk_db=True)()
Ag = material("aSi_i")()
aSi_i = material("aSi_p")()
aSi_p = material("aSi_n")()
aSi_n = material("LiF")()
LiF = material("IZO")()
IZO = material("C60")()
C60
# stack based on doi:10.1038/s41563-018-0115-4
= [
front_materials 100e-9, MgF2),
Layer(110e-9, IZO),
Layer(15e-9, C60),
Layer(1e-9, LiF),
Layer(440e-9, Perovskite),
Layer(6.5e-9, aSi_n),
Layer(6.5e-9, aSi_i),
Layer(
]
= [Layer(6.5e-9, aSi_i), Layer(6.5e-9, aSi_p), Layer(240e-9, ITO_back)] back_materials
Database file found at /Users/phoebe/.solcore/nk/nk.db
1 results found.
pageid shelf book page filepath hasrefractive hasextinction rangeMin rangeMax points
234 main MgF2 Rodriguez-de_Marcos main/MgF2/Rodriguez-de Marcos.yml 1 1 0.0299919 2.00146 960
Database file found at /Users/phoebe/.solcore/nk/nk.db
1 results found.
pageid shelf book page filepath hasrefractive hasextinction rangeMin rangeMax points
2 main Ag Jiang main/Ag/Jiang.yml 1 1 0.3 2.0 1701
Now we define our front and back surfaces, including interface_layers
. We will use regular pyramids for both the front and back surface; these pyramids point out on both sides, but since the direction of the pyramids is defined relative to the front surface, we must set upright=True
for the top surface and upright=False
for the rear surface. We also gives the surfaces a name (used to save the lookup table data) and ask RayFlare to calculate the absorption profile in the 5th layer, which is the perovskite.
= regular_pyramids(
triangle_surf =55,
elevation_angle=True,
upright=1,
size=front_materials,
interface_layers="coh_front",
name=[5],
prof_layers
)
= regular_pyramids(
triangle_surf_back =55,
elevation_angle=False,
upright=1,
size=back_materials,
interface_layers="Si_back",
name=["i"] * len(back_materials),
coherency_list )
Now we make our ray-tracing structure by combining the front and back surfaces, specifying the material in between (Si) and setting its width to 260 microns. In order to use the TMM lookuptables to calculate reflection/transmission/absorption probabilities we must also set use_TMM=True
.
= rt_structure(
rtstr_coh =[triangle_surf, triangle_surf_back],
textures=[Si],
materials=[260e-6],
widths=Air,
incidence=Ag,
transmission=True,
use_TMM=options,
options=True,
overwrite="current",
save_location
)
# calculate:
= rtstr_coh.calculate(options) result_coh
Pre-computing TMM lookup table(s)
Database file found at /Users/phoebe/.solcore/nk/nk.db
Material main/MgF2/Rodriguez-de Marcos.yml loaded.
Database file found at /Users/phoebe/.solcore/nk/nk.db
Material main/MgF2/Rodriguez-de Marcos.yml loaded.
Database file found at /Users/phoebe/.solcore/nk/nk.db
Material main/Ag/Jiang.yml loaded.
Database file found at /Users/phoebe/.solcore/nk/nk.db
Material main/Ag/Jiang.yml loaded.
Now we define the same front surface and structure again, except now we will treat all the layers incoherently (i.e. no thin-film interference) in the TMM.
= regular_pyramids(
triangle_surf =55,
elevation_angle=True,
upright=1,
size=front_materials,
interface_layers=["i"] * len(front_materials),
coherency_list="inc_front",
name=[5],
prof_layers
)
= rt_structure(
rtstr_inc =[triangle_surf, triangle_surf_back],
textures=[Si],
materials=[260e-6],
widths=Air,
incidence=Ag,
transmission=True,
use_TMM=options,
options=True,
overwrite="current",
save_location
)
= rtstr_inc.calculate(options) result_inc
Pre-computing TMM lookup table(s)
Now we plot the results for reflection, transmission, and absorption per layer for both the coherent and incoherent cases.
= sns.color_palette("husl", n_colors=len(front_materials) + len(back_materials) + 2)
pal # create a colour palette
= cycler("color", pal)
cols # set this as the default colour palette in matplotlib
= {
params "axes.prop_cycle": cols,
}
plt.rcParams.update(params)
= plt.figure(figsize=(8, 3.7))
fig 1, 1, 1)
plt.subplot(* 1e9, result_coh["R"], "-ko", label="R")
plt.plot(wavelengths * 1e9, result_coh["T"], mfc="none", label="T")
plt.plot(wavelengths * 1e9, result_coh["A_per_layer"][:, 0], "-o", label='Si')
plt.plot(wavelengths * 1e9, result_coh["A_per_interface"][0], "-o",
plt.plot(wavelengths =[None, "IZO", "C60", None, "Perovskite", None, None])
label* 1e9, result_coh["A_per_interface"][1], "-o",
plt.plot(wavelengths =[None, None, "ITO"])
label
* 1e9, result_inc["R"], "--ko", mfc="none")
plt.plot(wavelengths * 1e9, result_inc["T"], mfc="none")
plt.plot(wavelengths * 1e9, result_inc["A_per_layer"][:, 0], "--o", mfc="none")
plt.plot(wavelengths * 1e9, result_inc["A_per_interface"][0], "--o", mfc="none")
plt.plot(wavelengths * 1e9, result_inc["A_per_interface"][1], "--o", mfc="none")
plt.plot(wavelengths
300, 301], [0, 0], "-k", label="coherent")
plt.plot([300, 301], [0, 0], "--k", label="incoherent")
plt.plot(["Wavelength (nm)")
plt.xlabel("R / A / T")
plt.ylabel(0, 1)
plt.ylim(300, 1200)
plt.xlim(=(1.05, 1))
plt.legend(bbox_to_anchor
plt.tight_layout() plt.show()
Calculate and print the limiting short-circuit current per junction:
= q*np.trapz(result_coh["A_per_interface"][0][:,4]*AM15G.spectrum()[1],
Jmax_Pero_coh =wavelengths)/10
x= q*np.trapz(result_coh["A_per_layer"][:, 0]*AM15G.spectrum()[1],
Jmax_Si_coh =wavelengths)/10
x
print("Limiting short-circuit currents in coherent calculation (mA/cm2): {:.2f} / {:"
".2f}".format(Jmax_Pero_coh, Jmax_Si_coh))
= q*np.trapz(result_inc["A_per_interface"][0][:,4]*AM15G.spectrum()[1],
Jmax_Pero_inc =wavelengths)/10
x= q*np.trapz(result_inc["A_per_layer"][:, 0]*AM15G.spectrum()[1],
Jmax_Si_inc =wavelengths)/10
x
print("Limiting short-circuit currents in coherent calculation (mA/cm2): {:.2f} / {:"
".2f}".format(Jmax_Pero_inc, Jmax_Si_inc))
Limiting short-circuit currents in coherent calculation (mA/cm2): 19.90 / 20.94
Limiting short-circuit currents in coherent calculation (mA/cm2): 19.14 / 20.53
We can also plot the absorption profiles, for wavelengths up to 800 nm, in the perovskite (since we asked the solver to calculate the profile in the perovskite layer above).
= wavelengths < 800e-9
wl_Eg
= sns.cubehelix_palette(sum(wl_Eg), reverse=True)
pal = cycler("color", pal)
cols = {
params "axes.prop_cycle": cols,
}
plt.rcParams.update(params)
= np.arange(0, rtstr_coh.interface_layer_widths[0][4], options.depth_spacing*1e9)
pos
= plt.subplots(1,2)
fig, (ax1, ax2) "interface_profiles"][0][wl_Eg].T)
ax1.plot(pos, result_coh[0, 0.02)
ax1.set_ylim("z (nm)")
ax1.set_xlabel("a(z)")
ax1.set_ylabel("Coherent")
ax1.set_title("interface_profiles"][0][wl_Eg].T)
ax2.plot(pos, result_inc[0, 0.02)
ax2.set_ylim(
ax2.yaxis.set_ticklabels([])"z (nm)")
ax2.set_xlabel("Incoherent")
ax2.set_title( plt.show()
We see that, as expected, the coherent case shows interference fringes while the incoherent case does not. We can also plot the absorption profile in the Si (> 800 nm):
= pos = np.arange(0, rtstr_coh.widths[0]*1e6, options.depth_spacing_bulk*1e6)
pos_bulk
= plt.subplots(1,2)
fig, (ax1, ax2) "profile"][~wl_Eg].T)
ax1.semilogy(pos, result_coh[1e-8, 0.00015)
ax1.set_ylim("z (um)")
ax1.set_xlabel("a(z)")
ax1.set_ylabel("Coherent")
ax1.set_title("profile"][~wl_Eg].T)
ax2.semilogy(pos, result_inc[1e-8, 0.00015)
ax2.set_ylim(
ax2.yaxis.set_ticklabels([])"z (um)")
ax2.set_xlabel("Incoherent")
ax2.set_title( plt.show()
260.0
Questions
- Why do you think the total absorption is slightly lower in the incoherent calculation?
- Even though the layers on top of the Si are not very thick compared to the wavelength, and they are the first thing encountered by the light, why might it make sense to treat them incoherently?
- Can you improve the current-matching (at least in terms of limiting currents) between the perovskite and the Si?