from tlc import TLC, Trap, DefectData
import matplotlib.pyplot as plt
Detailed balance limit (Shockley-Queisser limit)
Shockley and Queisser proposed the efficiency limit of a single junction solar cell is a function of the band gap (original paper here). This approach is based on several assumptions:
All photons with energies equal or larger than the band gap are absorbed.
Each absorbed photon creates one electron-hole pair.
All charge carriers can reach their respective contacts.
Only radiative recombination (band-to-band electron-hole recombination) is considered.
Required inputs are:
Solar spectrum. The AM1.5g spectrum is used in this example (from NREL). But you can always input your specific spectrum.
Band gap of the bulk material.
Operation temperature
Tof the solar cell (with a default value of 300 K).
tlc_Sb2Se3_sq = TLC.sq_limit(1.419)
tlc_Sb2Se3_sq.calculate()
print(tlc_Sb2Se3_sq)
tlc_Sb2Se3_sq.results
The results property returns a dict with keys: j_sc, j0_rad, v_oc, v_max, j_max, ff, efficiency. Use to_dataframe() to get a single-row DataFrame (useful for parameter sweeps).
Radiative limit based on thickness-dependent absorptivity
If you want to replace the step-like absorptivity by the real absorption, pass an absorption coefficient via the
alpha=parameter.The absorptivity a is calculated as a=1-exp(-2αL), where L is the film thickness.
You can also pass a
pd.DataFramewith columnsEandalpha, or a NumPy array of shape (N, 2).
tlc_Sb2Se3_rad = TLC(1.419, T=300, thickness=500, alpha="Sb2Se3/alpha.csv")
tlc_Sb2Se3_rad.calculate()
print(tlc_Sb2Se3_rad)
Trap-limited conversion efficiency
To include the effects of point defects (Shockley-Read-Hall recombination), you need:
Equilibrium carrier concentrations (
n0,p0) and Fermi level — from a self-consistent Fermi level solver such asdopedorsc-fermi.Trap properties — capture coefficients for each defect transition, which can be calculated by CarrierCapture.jl or NonRad.
These are packaged into DefectData and Trap objects.
Choosing your workflow
TLC supports two ways to set up defect recombination:
Direct input — You supply carrier concentrations (n₀, p₀),
effective DOS (N_n, N_p), and capture coefficients directly. Use
this when you have values from experiment (e.g. Hall measurements),
from another Fermi level solver (e.g. sc-fermi), or when scanning
over artificial parameter ranges.
doped integration — You supply a doped DefectThermodynamics
object and capture coefficients; TLC extracts carrier concentrations
and defect populations automatically. Use this for fully
first-principles workflows where defect properties come from DFT.
Both workflows produce a DefectData object that is passed to
TLC(..., defect_data=data).
# Define a single-level trap (e.g. Se vacancy in Sb2Se3)
trap_VSe = Trap.single_level(
name="V_Se",
E_t=0.896, # trap level from VBM (eV)
N_t=1e15, # trap concentration (cm^-3)
q_initial=2,
q_final=1,
C_p=1.31e-9, # hole capture coefficient (cm^3/s)
C_n=7.91e-6, # electron capture coefficient (cm^3/s)
)
# Package equilibrium carrier data (e.g. from doped or sc-fermi)
defect_data = DefectData(
n0=1e6, # equilibrium electron concentration (cm^-3)
p0=1e16, # equilibrium hole concentration (cm^-3)
fermi_level=0.3, # Fermi level from VBM (eV)
e_gap=1.419, # band gap (eV)
temperature=300, # K
N_n=1e18, # effective CB DOS (cm^-3)
N_p=1e18, # effective VB DOS (cm^-3)
traps=[trap_VSe],
)
Alternative: construct DefectData from effective masses
If you know the electron and hole effective masses instead of the
carrier concentrations directly, use DefectData.from_effective_masses().
This computes N_n, N_p, n₀, and p₀ internally via the density of
states formula and Boltzmann statistics.
# Same trap as above, but carrier data derived from effective masses
defect_data_alt = DefectData.from_effective_masses(
fermi_level=0.3, # from VBM (eV)
e_gap=1.419, # band gap (eV)
temperature=300, # K
m_e=0.2, # electron effective mass (units of m₀)
m_h=0.8, # hole effective mass (units of m₀)
traps=[trap_VSe],
)
print(f"Computed n0 = {defect_data_alt.n0:.2e} cm⁻³")
print(f"Computed p0 = {defect_data_alt.p0:.2e} cm⁻³")
print(f"Computed N_n = {defect_data_alt.N_n:.2e} cm⁻³")
print(f"Computed N_p = {defect_data_alt.N_p:.2e} cm⁻³")
# One-step workflow: pass defect_data at construction time
tlc_Sb2Se3 = TLC(1.419, T=300, thickness=500, alpha="Sb2Se3/alpha.csv",
defect_data=defect_data)
tlc_Sb2Se3.calculate() # auto-computes SRH before J-V
print(tlc_Sb2Se3)
print(tlc_Sb2Se3.results)
tlc_Sb2Se3.to_dataframe()
Plotting functions
Absorption
tlc_Sb2Se3_rad.plot_alpha()
plt.xlim((0, 5))
J-V curve
fig, ax = plt.subplots()
tlc_Sb2Se3_rad.plot_jv(ax=ax)
tlc_Sb2Se3.plot_jv(ax=ax)
ax.legend(["Radiative limit", "With defects"])
Parameter sweep
Use to_dataframe() to collect results from multiple calculations into a single DataFrame. This is useful for studying the effect of thickness, band gap, or other parameters on efficiency.
import pandas as pd
results = []
for thickness in [100, 200, 500, 1000, 2000]:
t = TLC(1.419, thickness=thickness, alpha="Sb2Se3/alpha.csv")
t.calculate()
results.append(t.to_dataframe())
df = pd.concat(results, ignore_index=True)
df
Using doped for carrier concentrations
If your defect calculations were processed with
doped, you can extract equilibrium
carrier concentrations and defect populations directly from a
DefectThermodynamics object using defect_data_from_doped().
This requires pip install doped (or pip install -e ".[doped]").
The key additional input you need is trap_config — a dictionary
mapping defect names (matching those in your doped calculation) to
their capture coefficients from
CarrierCapture.jl
or NonRad.
# === This cell requires doped and real DFT data ===
# Shown here as a reference for the doped workflow.
from doped.thermodynamics import DefectThermodynamics
from tlc import TLC
from tlc.doped_interface import defect_data_from_doped
# Load your doped results
thermo = DefectThermodynamics.from_json("path/to/defect_thermo.json")
# Map defect names to capture coefficients
# Keys must match defect names in your doped calculation
trap_config = {
"V_Se": {
"transitions": [
{
"q1": 2, # initial charge state
"q2": 1, # final charge state
"E_t1": 0.896, # trap level from VBM (eV)
"g": 1, # degeneracy factor
"C_p1": 1.31e-9, # hole capture coefficient (cm³/s)
"C_n1": 7.91e-6, # electron capture coefficient (cm³/s)
},
# Add more transitions for the same defect if needed
],
},
# Add more defects as needed
}
# Extract carrier concentrations + build DefectData automatically
data = defect_data_from_doped(
thermo,
trap_config,
temperature=300, # operating temperature (K)
anneal_temperature=853, # for frozen-defect approximation (K)
)
# Same TLC workflow from here
t = TLC(1.419, thickness=500, alpha="Sb2Se3/alpha.csv", defect_data=data)
t.calculate()
print(t)
The trap_config dictionary maps each defect name to a list of
transitions. Each transition specifies:
q1,q2: initial and final charge statesE_t1: trap energy level from VBM (eV)C_p1,C_n1: hole and electron capture coefficients (cm³/s)g: degeneracy factor (default 1)
For two-level (three charge state) traps, also include q3, E_t2,
C_p2, and C_n2.
The function uses anneal_temperature for the frozen-defect
approximation: defect concentrations are set at the annealing
temperature, then carrier concentrations are recalculated at the
operating temperature. If anneal_temperature is not provided,
equilibrium at temperature is assumed.