Emission spectra in OghmaNano
An emission spectrum describes how the light emitted by a material is distributed over wavelength. OghmaNano stores these spectra as part of its Materials database, where they can be inspected, imported or updated. This page explains what an emission spectrum represents, how it is stored and imported, and finally how to generate an approximate spectrum using Python when measured data are unavailable.
1. What is an emission spectrum?
When electrons and holes recombine radiatively, photons are emitted over a range of energies rather than at one exact wavelength. An emission spectrum describes the relative intensity of this light as a function of wavelength. It can come from an experiment, such as photoluminescence (PL) or electroluminescence (EL), or from a theoretical model. The spectrum is not the same as the material's absorption coefficient or refractive index.
The spectral shape depends on the semiconductor's electronic structure, temperature, composition and, in a real device, carrier populations and optical effects. A normalised emission spectrum represents relative wavelength distribution, not absolute optical power or LED efficiency.
2. Emission spectra in the materials database
Each material has its own directory in the OghmaNano Materials database.
When emission data are available, they are stored in a file named
emission.csv alongside files such as alpha.csv
and n.csv. For example, a material folder may contain:
materials/
GaAs/
emission.csv
alpha.csv
n.csv
The materials database used by simulations is located beneath the user's
oghma_local/materials directory. For a description of the
directory structure and material-file formats, see
Materials database: Part C – How materials are stored.
For an introduction to browsing and editing materials, see
Materials database: Part A.
An emission.csv file contains the wavelength in metres and the
relative emission intensity in arbitrary units. OghmaNano-format files also
contain a metadata header; the header's y_len value gives the
number of wavelength–intensity data points that follow it.
3. Importing measured emission spectra
Where measured photoluminescence (PL) or electroluminescence (EL) data exist, these are preferable to an approximate generated spectrum, provided the measurements are appropriate for the material and intended device. Open the relevant material in the Materials database, select its emission spectrum, and use the data-import facility to import the measured curve. The general process for importing a two-column dataset, selecting wavelength units and reviewing the converted values is described in Materials database: Part A – Importing data.
Units: The stored wavelength values are in metres, even if the graph is displayed in nanometres. Use normalised intensity (a.u.) for representative emission-shape data. Check the file's metadata and the number of points after importing or generating a spectrum.
4. Estimating emission spectra when measured data are unavailable
If no suitable measured spectrum is available, the Python script below can generate a temporary, normalised bulk semiconductor emission spectrum. It uses a simplified thermal band-to-band model for direct-gap materials:
\(S_E(E) \propto \sqrt{E-E_g}\,\exp[-(E-E_g)/(k_B T)]\), for \(E \geq E_g\).
Here \(E_g\) is the bandgap and \(T\) is the temperature.
The script converts the calculated energy-domain emission profile to
intensity per unit wavelength, normalises its peak to one, and writes
emission.csv in the same metadata-and-data format used by OghmaNano.
By default it creates 2,036 wavelength–intensity points at 300 K.
The supplied script creates one directory for each of 29 distinct semiconductor entries, including GaAs, AlGaAs, GaN, InGaN, InP and InGaAs. Material bandgaps are representative starting values; for composition-dependent alloys, a generated spectrum represents only one nominal composition.
When is this appropriate? Use these generated curves as illustrative bulk emission profiles, particularly for testing the emission pipeline or until better data or a band-structure-based calculation is available. They are not measured spectra and should not be represented as quantitative predictions of a particular LED.
The model does not include quantum-well confinement, alloy disorder, excitonic features, defect emission, strain or optical outcoupling. It is especially unsuitable for indirect-gap transitions, including the GaP entry: its generated curve represents an illustrative direct transition rather than conventional GaP LED emission.
5. Example: generating emission spectra with Python
Save the code below as generate_emission_spectra.py and run:
python3 generate_emission_spectra.py
The default output directory is emission_generated.
To select another directory or change the number of points:
python3 generate_emission_spectra.py --output my_emission_database --points 2036
To change the operating temperature or supply your own bandgaps:
python3 generate_emission_spectra.py --temperature 300 --bandgap-file bandgaps.json
An example bandgaps.json file is:
{
"GaAs": 1.424,
"AlGaAs": 1.80,
"InGaN": 2.75
}
Inspect the resulting spectra before placing the appropriate
emission.csv files in the corresponding material folders.
The bandgaps in this script are manually specified representative values;
the script does not call the Lua material scripts to calculate them.
6. Complete Python source code
The following is the complete generator, using only Python's standard library. Expand the source to copy it into a local Python file.
Show the complete Python emission spectrum generator
#!/usr/bin/env python3
"""Generate illustrative bulk band-edge emission.csv files for OghmaNano.
No external Python dependencies. Each directory contains a 2036-point
OghmaNano-format emission.csv. Spectra are normalised per wavelength and
are NOT measured or k.p-calculated spectra. Alloy bandgaps below represent
one nominal composition only; edit MATERIALS or use --bandgap-file.
"""
import argparse
import json
import math
from pathlib import Path
# Representative, approximate 300 K bulk DIRECT-transition gaps (eV).
# NOT universal values for alloys. Entries such as AlInP and AlInAs may be
# indirect at common compositions: their curves are illustrative ONLY.
# One directory per unique material; InSb was duplicated in the supplied list.
MATERIALS = {
"InP": (1.344, "InP/InP.lua"),
"GaP": (2.78, "GaP/GaP.lua"), # direct transition; GaP fundamental gap is indirect!
"AlGaInP": (2.05, "AlGaInP.lua"),
"GaAs": (1.424, "GaAs/GaAs.lua"),
"AlInP": (2.35, "AlInP.lua"), # illustrative; verify direct-gap composition
"InSb": (0.17, "InSb/InSb.lua"),
"GaSb": (0.726, "GaSb/GaSb.lua"),
"CdS": (2.42, "CdS/CdS.lua"),
"GaN": (3.40, "GaN/GaN.lua"),
"AlN": (6.03, "AlN.lua"),
"ZnS": (3.68, "ZnS.lua"),
"InAs": (0.354, "InAs/InAs.lua"),
"AlGaAs": (1.80, "AlGaAs/AlGaAs.lua"), # nominal Al x~0.3
"InAsP": (0.95, "InAsP.lua"),
"InGaAsP": (0.80, "InGaAsP.lua"),
"AlInAs": (1.45, "AlInAs.lua"), # composition-dependent and possibly indirect
"GaAsP": (1.90, "GaAsP.lua"),
"InGaAs": (0.75, "InGaAs/InGaAs.lua"), # nominal In0.53Ga0.47As
"ZnTe": (2.26, "ZnTe.lua"),
"InGaN": (2.75, "InGaN.lua"),
"GaInNAs": (0.95, "GaInNAs.lua"),
"InGaP": (1.90, "InGaP.lua"),
"GaInAs": (0.75, "GaInAs.lua"),
"InN": (0.69, "InN.lua"),
"AlGaN": (4.43, "AlGaN.lua"),
"AlGaInAs": (0.80, "AlGaInAs.lua"),
"CdSe": (1.74, "CdSe.lua"),
"ZnSe": (2.70, "ZnSe.lua"),
"CdTe": (1.50, "CdTe/CdTe.lua"),
}
HC_EV_NM = 1239.8419843320026
KB_EV_K = 8.617333262145e-5
def make_spectrum(eg, temp, points):
"""3D nondegenerate direct-gap model, intensity per unit wavelength.
S_E(E) ~ sqrt(E-Eg)*exp(-(E-Eg)/kT), E>=Eg.
Convert using dE/dlambda ~ 1/lambda**2, then peak-normalise.
Span covers Eg to Eg + max(0.35 eV, 12 kT); wavelength increases.
"""
kt = KB_EV_K * temp
upper = eg + max(0.35, 12.0 * kt)
lo_nm = HC_EV_NM / upper
hi_nm = HC_EV_NM / eg
values = []
for i in range(points):
wavelength_nm = lo_nm + (hi_nm - lo_nm) * i / (points - 1)
e = HC_EV_NM / wavelength_nm
de = max(0.0, e - eg)
intensity = math.sqrt(de) * math.exp(-de / kt) / (wavelength_nm * wavelength_nm)
values.append((wavelength_nm * 1e-9, intensity))
top = max(v for _, v in values)
return [(lam, value / top) for lam, value in values]
def generate(out, material, eg, lua_path, temp, points):
folder = out / material
folder.mkdir(parents=True, exist_ok=True)
metadata = {
"title": "Illustrative band-edge emission of " + material,
"type": "xy",
"y_label": "Wavelength",
"data_label": "Intensity",
"y_units": "nm",
"y_mul": 1e9,
"data_units": "au",
"icon": "spectra_file",
"time ": 0.0,
"Vexternal": 0.0,
"x_len": 1,
"y_len": points,
"z_len": 1,
"cols": "yd",
}
values = make_spectrum(eg, temp, points)
path = folder / "emission.csv"
with path.open("w", encoding="utf-8", newline="\n") as f:
f.write("#oghma_csv " + json.dumps(metadata, ensure_ascii=False, separators=(",", ":")) + "*\n")
for lam, value in values:
f.write(f"{lam:.12e}\t{value:.12e}\n")
return path
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, default=Path("emission_generated"))
parser.add_argument("--points", type=int, default=2036)
parser.add_argument("--temperature", type=float, default=300.0)
parser.add_argument("--bandgap-file", type=Path,
help='Optional JSON map e.g. {"GaAs":1.424,"AlGaAs":1.80} to override nominal gaps')
args = parser.parse_args()
if args.points < 3 or args.temperature <= 0:
parser.error("--points must be >=3 and --temperature must be positive")
gap_overrides = {}
if args.bandgap_file:
gap_overrides = json.loads(args.bandgap_file.read_text(encoding="utf-8"))
unknown = set(gap_overrides) - set(MATERIALS)
if unknown:
parser.error("Unknown material(s): " + ", ".join(sorted(unknown)))
args.output.mkdir(parents=True, exist_ok=True)
for material, (nominal_eg, lua_path) in MATERIALS.items():
eg = float(gap_overrides.get(material, nominal_eg))
if not math.isfinite(eg) or eg <= 0:
parser.error(f"Invalid bandgap for {material}: {eg}")
path = generate(args.output, material, eg, lua_path, args.temperature, args.points)
print(f"{path} Eg={eg:g} eV (source: {lua_path})")
print(f"Generated {len(MATERIALS)} unique material directories with {args.points} points each.")
print("CAUTION: nominal/illustrative bulk direct-transition profiles; validate alloy composition and gap type.")
if __name__ == "__main__":
main()
Note: Generated emission spectra are a stopgap solution. For quantitative device simulations, prefer measured emission data or spectra calculated using an appropriate electronic band-structure and optical-transition model, such as a k·p-based calculation.