Home Examples Screenshots User manual Bluesky logo YouTube ☰
OghmaNano Multiphysics simulation platform for optoelectronic devices and photonic systems DOWNLOAD Quick Start guide

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.