Strona główna Przykłady Zrzuty ekranu Instrukcja obsługi Logo Bluesky. YouTube ☰
OghmaNano Platforma do symulacji wielofizycznych procesów zachodzących w optoelektronicznych urządzeniach i systemach fotonicznych. POBIERZ Przewodnik szybkiego uruchomienia

Baza danych widm optycznych

Przegląd / Omówienie

Optyczna baza danych w OghmaNano przechowuje widma dla realistycznych źródeł światła, w tym światła słonecznego, laserów oraz zmierzonych eksperymentalnie widm oświetlenia. Każde widmo opisuje, jak intensywność źródła światła zmienia się w zależności od długości fali. Dane te są zazwyczaj przechowywane w postaci dwóch kolumn: długość fali w metrach (m) i promieniowanie spektralne w W m−2 m−1. Całkowanie widma w zakresie długości fali daje całkowitą moc optyczną padającą na jednostkę powierzchni, mierzoną w W m−2.

Po wybraniu określonego spektrum jako źródła oświetlenia, OghmaNano wykorzystuje je do obliczenia zależności optycznej generacji światła wewnątrz urządzenia od długości fali. Ustawienie „Natężenie światła (w jednostkach odpowiadających natężeniu słonecznemu)” pełni funkcję mnożnika dla wybranego spektrum. Wartość 1,0 oznacza wykorzystanie pierwotnego natężenia spektralnego, 0,5 zmniejsza je o połowę, a 0,0 wyłącza oświetlenie. Dzięki temu można wykorzystać to samo spektrum źródła światła przy różnych poziomach natężenia oświetlenia, bez konieczności modyfikowania podstawowych danych.

Przeglądanie bazy danych optycznych.

Aby przejrzeć dostępne widma, kliknij ikonę Bazy danych optycznych na pasku narzędzi Optyka (??). Otworzy to przeglądarkę bazy danych optycznych (??), w której można wyświetlać, importować i wybierać widma źródeł światła do wykorzystania w symulacjach.

OghmaNano Databases ribbon with the Optical database icon highlighted.
Na wstążce „Bazy danych” w programie OghmaNano wybierz opcję „Baza danych optycznych”. Otwiera to bazę danych widm optycznych.
OghmaNano window showing the Optical database directory with available spectra entries.
Katalog bazy danych optycznych, zawierający listę dostępnych widm. Kliknięcie dwukrotne na wpis otwiera jego dane w edytorze.

Kliknięcie dwukrotne na wykresie spektralnym otwiera go w edytorze wykresów spektralnych (??), gdzie można wyświetlić istniejące dane lub zaimportować nowe wykresy. Zakładka „Podstawowe” (??) zawiera pola do wprowadzania notatek, metadanych i ustawień dotyczących prywatności.

OghmaNano optical spectrum editor showing a solar spectrum plot of spectral irradiance versus wavelength.
Zakładka „Spectra” w edytorze widma optycznego, wyświetlająca promieniowanie słoneczne w funkcji długości fali.
OghmaNano optical spectrum editor showing the Basic tab with metadata fields.
Zakładka „Podstawowe” w edytorze widma optycznego. Użyj jej, aby dodać notatki, ustawić poziom prywatności i wprowadzić opisowe metadane dotyczące widma.

Importowanie danych widmowych

OghmaNano umożliwia użytkownikom importowanie własnych widm źródeł światła, w tym zmierzonych widm promieniowania słonecznego, profili emisji laserowej oraz innych danych eksperymentalnych. Aby zaimportować widmo, otwórz edytor widm optycznych i kliknij opcję „Z pliku”. Otworzy się okno importu danych (??). Wybierz plik za pomocą opcji „Otwórz plik danych”, a następnie określ kolumny zawierające długość fali i natężenie oraz podaj jednostki, w jakich są wyrażone. Na przykład, długości fal mogą być podane w nanometrach, a nie w metrach. OghmaNano automatycznie konwertuje zaimportowane wartości na jednostki SI. Kliknij „Importuj dane”, aby załadować przekonwertowane widmo do edytora.

Zaimportowane widma są zapisywane w lokalnej bazie danych optycznych pod nazwą oghma_local/spectra/. Każde widmo ma swój własny katalog, co przedstawiono na ilustracji ??. Samo widmo jest przechowywane w pliku spectra.csv, który można również otworzyć bezpośrednio w edytorze tekstu (??).

OghmaNano data import window showing wavelength and intensity data being converted into SI units.
Okno importu danych wyświetla surowe dane wejściowe oraz odpowiadające im wartości przeliczone na jednostki układu SI.
File browser showing an OghmaNano optical spectrum directory containing JSON, CSV and bibliography files.
Katalog zawierający spektrum, w skład którego wchodzą data.json, spectra.csv oraz opcjonalny plik bibliograficzny.
Text editor displaying an OghmaNano spectra.csv file with its header and wavelength-intensity data.
Zawartość pliku spectra.csv zawiera informacje o nagłówku pliku oraz widmie promieniowania w zależności od długości fali.

W jaki sposób zapisywane są widma źródeł światła?

Każdy widm w bazie danych optycznych OghmaNano jest przechowywany w osobnym katalogu, który zawiera dwa główne pliki: data.json i spectra.csv. Opcjonalnie można dołączyć plik spectra.bib, który zawiera bibliograficzne odniesienia. Plik data.json identyfikuje katalog jako widmo źródła światła, wykorzystując pole "item_type":"spectra". Numeryczne dane widmowe są przechowywane oddzielnie w pliku spectra.csv.

Plik spectra.csv rozpoczyna się od nagłówka #oghma_csv, który zawiera opis danych, a następnie następuje dwie kolumny wartości liczbowych. W pierwszej kolumnie znajdują się długości fal wyrażone w metrach (m), a w drugiej – widmowa promieniście wyrażona w W m−2 m−1. Chociaż długości fal są przechowywane w metrach, pole y_mul w nagłówku umożliwia programowi OghmaNano wyświetlanie ich w nanometrach. Poniższy przykład przedstawia początek spektrum słonecznego AM1.5G:

#oghma_csv {"title":"Light intensity of AM 1.5 Sun","type":"xy","y_label":"Wavelength","data_label":"Intensity","y_units":"nm","y_mul":1000000000.0,"data_units":"m^{-1}.W.m(^-2)","icon":"spectra_file","time ":0.0,"Vexternal":0.0,"x_len":1,"y_len":2002,"z_len":1,"cols":"yd"}*
2.800000e-07    4.730900e-14
2.805000e-07    1.230700e-12
2.810000e-07    5.689500e-12
2.815000e-07    1.566200e-10
2.820000e-07    1.194600e-09
2.825000e-07    4.543600e-09
2.830000e-07    1.845200e-08
2.835000e-07    3.536000e-08
2.840000e-07    7.267000e-07
2.845000e-07    2.485600e-06
...

Ważnym polem w nagłówku jest y_len, które określa liczbę punktów danych odpowiadających długości fali i intensywności. Liczba ta musi odpowiadać liczbie wierszy danych w pliku. W powyższym przykładzie y_len wynosi 2002, ponieważ pełne spektrum AM1.5G zawiera 2002 punkty danych.

Pole powierzchni pod krzywą rozkładu promieniowania widmowego odpowiada całkowitej mocy promieniowania optycznego przypadającej na jednostkę powierzchni:

\[ P_{\mathrm{in}} = \int I_{\lambda}(\lambda)\,d\lambda \]

W tym przypadku \(I_{\lambda}\) oznacza promieniowanie widmowe wyrażone w W m−2 m−1, a wynikowa gęstość mocy jest mierzona w W m−2. Na przykład, standardowe spektrum słoneczne AM1.5G ma całkowitą wartość około 1000 W m−2. Ważne jest, aby importowane widma miały poprawną zintegrowaną intensywność, ponieważ OghmaNano wykorzystuje wartości zawarte w pliku bezpośrednio, zamiast automatycznie je normalizować.

Użytkownicy mogą również ręcznie tworzyć własne widma, bez korzystania z narzędzia do importowania. Najprostszym sposobem jest skopiowanie istniejącego katalogu widm, zastąpienie wartości długości fali i natężenia w pliku spectra.csv oraz zaktualizowanie informacji w nagłówku, szczególnie w sekcji y_len. Plik data.json można zachować z oryginalnego katalogu. Dzięki temu można w prosty sposób zdefiniować niestandardowe widma laserowe, źródła monochromatycznego oświetlenia lub eksperymentalnie zmierzone źródła światła.

Windows: szybko otwórz lokalny folder ze spektrami.

  1. Prasa Wygraj+Rtyp %USERPROFILE%\oghma_local\spectrai naciśnij Wejdź/Wpisz.
  2. Każdy widm jest zapisywany w oddzielnym podfolderze (np. ...\spectra\example\).

Generator spektrum słonecznego

Generator widma słonecznego (??) może być uruchomiony z poziomu edytora widma optycznego. Porównuje on referencyjne widmo AM1.5G z obliczonymi składowymi Iglobal, Idirect i Idiffuse, a także umożliwia generowanie widm dla różnych pór dnia, lokalizacji geograficznych i warunków atmosferycznych (ciśnienie, zawartość pary wodnej i stężenie aerozoli).

Generator opiera się na prostym modelu spektralnym promieniowania słonecznego, uwzględniającym promieniowanie bezpośrednie i rozproszone na poziomej i nachylonej powierzchni na powierzchni Ziemi, w warunkach bezchmurnej atmosfery, opracowanym przez Birda i Riordana (1986) i opublikowanym w czasopiśmie „Journal of Applied Meteorology and Climatology”. Oryginalny artykuł można znaleźć tutaj. Ten częściowo empiryczny model oblicza spektralną intensywność promieniowania słonecznego na powierzchni Ziemi, uwzględniając kluczowe procesy atmosferyczne, takie jak rozpraszanie Rayleigha, absorpcja ozonu, obecność jednorodnie rozmieszczonych gazów, absorpcja pary wodnej i ekstynkcja aerozoli. Model, wykorzystując dane wejściowe, takie jak lokalizacja, pora dnia, wysokość i parametry atmosferyczne, dzieli spektrum słoneczne na składowe globalną, bezpośrednią i rozproszoną. Referencyjny model AM1.5G jest dostarczany jako punkt odniesienia, umożliwiając użytkownikom generowanie spektrów specyficznych dla danego miejsca i warunków, które odzwierciedlają zarówno czynniki geometryczne (kąt zenitalny Słońca, masa powietrza), jak i właściwości atmosferyczne (ciśnienie, zawartość pary wodnej i głębokość optyczną aerozoli).

OghmaNano Solar Spectrum Generator window showing AM1.5G reference spectrum compared with calculated global, direct, and diffuse components.
Okno „Generator spektrum słonecznego”. Porównaj referencyjne spektrum AM1.5G z obliczonymi składowymi (Iglobal, Idirect, Idiffuse) i generuj nowe spektrum w różnych warunkach.

Tworzenie niestandardowych widm laserowych za pomocą języka Python.

Oprócz możliwości importowania eksperymentalnie zmierzonych widm, OghmaNano umożliwia użytkownikom generowanie własnych widm źródeł światła za pomocą zewnętrznych skryptów. Na przykład, poniższy program w języku Python generuje realistyczne widma emisji laserowej i zapisuje je bezpośrednio w formacie bazy danych optycznych OghmaNano. Jest to przydatne podczas symulacji urządzeń oświetlanych laserami, takich jak fotodiody InGaAs działające przy długości fali 1550 nm, czujniki optyczne i półprzewodnikowe fotodetektory.

Skrypt obsługuje trzy różne modele laserów: laser o spektrum gaussowskim lub lorentzowskim, przeznaczony do prostych źródeł o wąskim paśmie, laser Fabry’ego-Pérota z wieloma modami podłużnymi oraz laser z rozproszonym sprzężeniem zwrotnym (DFB) z dominującym szczytem emisji i słabszymi modami bocznymi. Opcjonalnie można również uwzględnić tło w postaci wzmocnionej spontanicznej emisji (ASE). Długość fali, szerokość spektralna i całkowita moc optyczna mogą być regulowane za pomocą poleceń wprowadzanych w wierszu poleceń.

Aby użyć skryptu, zapisz poniższy kod jako make_laser_spectrum.py. Wymaga on Pythona 3 i biblioteki NumPy, przy czym Matplotlib jest potrzebny tylko podczas generowania przykładowego wykresu. Uruchomienie skryptu bez żadnych argumentów powoduje utworzenie katalogu o nazwie laser_1550nm, który zawiera data.json i spectra.csv. Domyślnie generowane spektrum jest wyśrodkowane przy długości fali 1550 nm, ma rozkład Gaussa o szerokości na poziomie połowy maksimum (FWHM) wynoszącej 2 nm i charakteryzuje się zintegrowaną gęstością mocy optycznej wynoszącą 10 W/m².

python3 make_laser_spectrum.py

Inne modele laserów można uzyskać, modyfikując argumenty w wierszu poleceń. Na przykład, następujące polecenie generuje widmo lasera Fabry’ego-Pérota z wieloma modami podłużnymi i szumem spontanicznej emisji (ASE) o poziomie 40 dB poniżej wartości maksymalnej:

python3 make_laser_spectrum.py fp_1550 --model fp --cavity-length 300 --ase-db 40

Można również uzyskać laser DFB o wąskim zakresie częstotliwości, stosując:

python3 make_laser_spectrum.py dfb_1550 --model dfb --fwhm 0.1 --smsr 40 --plot

Wygenerowane spektrum jest automatycznie normalizowane, tak aby całka jego widmowej rozdzielczości odpowiadała określonej gęstości mocy optycznej. Skrypt oblicza również liczbę punktów odpowiadających różnym długościom fal i zapisuje poprawną wartość y_len w nagłówku #oghma_csv. Dzięki temu możliwe jest generowanie spektrów bez ręcznej edycji żadnego z plików wyjściowych.

Po wygenerowaniu skopiuj utworzony katalog do folderu oghma_local/spectra/. Nowe źródło światła będzie dostępne w bazie danych optycznych OghmaNano. Ponieważ dane wyjściowe są zapisywane w standardowym formacie widma OghmaNano, można zastosować tę samą procedurę do utworzenia biblioteki źródeł laserowych o różnych długościach fal i mocach.

🐍 Wyświetl kompletny generator spektrum laserowego w języku Python.
#!/usr/bin/env python3
"""
make_laser_spectrum.py - generate an OghmaNano spectrum file for a laser source.

Output is an OghmaNano spectra directory:
    <outdir>/data.json    spectra item metadata
    <outdir>/spectra.csv  #oghma_csv {json header}*
                          <wavelength in m>\t<spectral irradiance in W m^-2 m^-1>

If <outdir> exists it is reused; only data.json and spectra.csv are
overwritten, nothing is deleted.

The spectrum is normalised so that the integral over wavelength (in metres)
equals --power (W/m^2). Default is 10 W/m^2.

Models
------
gaussian : single line, Gaussian (or Lorentzian) of FWHM --fwhm.
fp       : Fabry-Perot multimode laser. Longitudinal modes spaced by
           d_lambda = lambda^2 / (2 n_g L) under a Gaussian gain envelope of
           FWHM --fwhm. Each mode has width --mode-width (effectively the
           OSA resolution; true FP mode linewidths are ~1e-4 nm).
dfb      : DFB laser. Main line of FWHM --fwhm plus two side modes at the
           grating stopband edges (lambda0 +/- stopband/2), suppressed by
           --smsr dB, with optional extra suppression of one side
           (--side-asym) since real DFBs usually lase with one dominant
           side mode.

Any model can add a broad amplified-spontaneous-emission (ASE) pedestal
--ase-db dB below the peak with FWHM --ase-fwhm.

Examples
--------
  # no arguments: 1550 nm, 2 nm FWHM Gaussian, 10 W/m^2 -> ./laser_1550nm/
  python3 make_laser_spectrum.py

  # Fabry-Perot, 300 um InGaAsP cavity, 2 nm envelope, ASE 40 dB down
  python3 make_laser_spectrum.py fp_1550 --model fp --cavity-length 300 --ase-db 40

  # DFB, 0.1 nm line, 40 dB SMSR, 3 nm stopband, preview plot
  python3 make_laser_spectrum.py dfb_1550 --model dfb --fwhm 0.1 --smsr 40 --plot
"""

import argparse
import json
import math
import os
import sys

import numpy as np

LN2 = math.log(2.0)
_trapz = getattr(np, "trapezoid", None) or np.trapz

# data.json for an OghmaNano spectra item, written verbatim.
DATA_JSON = """{
\t"item_type":"spectra",
\t"status":"public",
\t"changelog":"",
\t"spectra_import": {
\t\t"import_file_path":"none",
\t\t"import_x_combo_pos":9,
\t\t"import_data_combo_pos":5,
\t\t"import_x_spin":0,
\t\t"import_data_spin":2,
\t\t"import_title":"Voltage - J",
\t\t"import_xlabel":"Voltage",
\t\t"import_data_label":"J",
\t\t"import_area":0.104,
\t\t"import_data_invert":"False",
\t\t"import_x_invert":"False",
\t\t"data_file":"none"
\t\t}
\t}
"""


def gaussian(x, x0, fwhm):
    return np.exp(-4.0 * LN2 * ((x - x0) / fwhm) ** 2)


def lorentzian(x, x0, fwhm):
    hw = 0.5 * fwhm
    return hw * hw / ((x - x0) ** 2 + hw * hw)


def line(x, x0, fwhm, shape):
    return gaussian(x, x0, fwhm) if shape == "gaussian" else lorentzian(x, x0, fwhm)


def mode_spacing_nm(a):
    if a.mode_spacing is not None:
        return a.mode_spacing
    lam = a.wavelength * 1e-9
    L = a.cavity_length * 1e-6
    return lam * lam / (2.0 * a.group_index * L) * 1e9


def build(a):
    """Return (wavelength_nm, relative_intensity) before power normalisation."""
    lam0, fwhm = a.wavelength, a.fwhm

    # Window half-width: enough to make the line tails negligible.
    tail = 5.0 if a.shape == "gaussian" else 50.0
    half = tail * fwhm

    if a.model == "fp":
        half = max(half, 3.0 * fwhm + tail * a.mode_width)

    if a.model == "dfb":
        half = max(half, 0.5 * a.stopband + tail * fwhm)

    if a.ase_db is not None:
        half = max(half, 2.0 * a.ase_fwhm)

    if a.window is not None:
        half = 0.5 * a.window

    # Step: resolve the narrowest feature with ~20 points per FWHM.
    narrowest = a.mode_width if a.model == "fp" else fwhm
    step = a.step if a.step is not None else narrowest / 20.0

    n = int(round(2.0 * half / step)) + 1

    if n > 500000:
        sys.exit(f"error: {n} points needed; increase --step or reduce --window")

    lam = np.linspace(lam0 - half, lam0 + half, n)

    if a.model == "gaussian":
        I = line(lam, lam0, fwhm, a.shape)

    elif a.model == "fp":
        dl = mode_spacing_nm(a)
        kmax = int(half / dl) + 1
        I = np.zeros_like(lam)

        for k in range(-kmax, kmax + 1):
            lk = lam0 + k * dl
            I += gaussian(lk, lam0, fwhm) * line(
                lam, lk, a.mode_width, a.shape
            )

    elif a.model == "dfb":
        s = 10.0 ** (-a.smsr / 10.0)
        s_short = s * 10.0 ** (-a.side_asym / 10.0)

        I = (
            line(lam, lam0, fwhm, a.shape)
            + s * line(
                lam, lam0 + 0.5 * a.stopband, fwhm, a.shape
            )
            + s_short * line(
                lam, lam0 - 0.5 * a.stopband, fwhm, a.shape
            )
        )

    if a.ase_db is not None:
        I = I + I.max() * 10.0 ** (-a.ase_db / 10.0) * gaussian(
            lam, lam0, a.ase_fwhm
        )

    # Zero anchors just outside the window so interpolation beyond it gives 0.
    lam = np.concatenate(([lam[0] - step], lam, [lam[-1] + step]))
    I = np.concatenate(([0.0], I, [0.0]))

    return lam, I


def measured_fwhm(lam, I):
    """FWHM of the outer half-maximum crossings (envelope width for fp)."""
    above = np.where(I >= 0.5 * I.max())[0]
    i0, i1 = above[0], above[-1]

    l = np.interp(
        0.5 * I.max(),
        [I[i0 - 1], I[i0]],
        [lam[i0 - 1], lam[i0]]
    )

    r = np.interp(
        0.5 * I.max(),
        [I[i1 + 1], I[i1]],
        [lam[i1 + 1], lam[i1]]
    )

    return r - l


def write_oghma(path, lam_m, I, title):
    header = {
        "title": title,
        "type": "xy",
        "y_label": "Wavelength",
        "data_label": "Intensity",
        "y_units": "nm",
        "y_mul": 1000000000.0,
        "data_units": "m^{-1}.W.m(^-2)",
        "icon": "spectra_file",
        "time ": 0.0,
        "Vexternal": 0.0,
        "x_len": 1,
        "y_len": int(len(lam_m)),
        "z_len": 1,
        "cols": "yd",
    }

    with open(path, "w") as f:
        f.write(
            "#oghma_csv "
            + json.dumps(header, separators=(",", ":"))
            + "*\n"
        )

        for l, v in zip(lam_m, I):
            f.write(f"{l:e}\t{v:e}\n")


def plot(path, lam_nm, I):
    import matplotlib
    matplotlib.use("Agg")
    import matplotlib.pyplot as plt

    fig, (ax1, ax2) = plt.subplots(
        2, 1, figsize=(7, 6), sharex=True
    )

    ax1.plot(lam_nm, I)
    ax1.set_ylabel("W m$^{-2}$ m$^{-1}$")

    db = 10.0 * np.log10(
        np.maximum(I, I.max() * 1e-8) / I.max()
    )

    ax2.plot(lam_nm, db)
    ax2.set_ylabel("dB rel. peak")
    ax2.set_xlabel("Wavelength (nm)")
    ax2.set_ylim(-80, 3)

    fig.tight_layout()
    fig.savefig(path, dpi=120)


def main():
    p = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.RawDescriptionHelpFormatter
    )

    p.add_argument(
        "outdir", nargs="?", default="laser_1550nm",
        help="output directory (default: laser_1550nm)"
    )

    p.add_argument(
        "--model",
        choices=["gaussian", "fp", "dfb"],
        default="gaussian"
    )

    p.add_argument(
        "--wavelength",
        type=float,
        default=1550.0,
        help="centre, nm"
    )

    p.add_argument(
        "--fwhm",
        type=float,
        default=2.0,
        help="line FWHM (gaussian/dfb) or gain-envelope FWHM (fp), nm"
    )

    p.add_argument(
        "--power",
        type=float,
        default=10.0,
        help="integrated irradiance, W/m^2"
    )

    p.add_argument(
        "--shape",
        choices=["gaussian", "lorentzian"],
        default="gaussian",
        help="lineshape of each line/mode"
    )

    # Fabry-Perot
    p.add_argument(
        "--cavity-length",
        type=float,
        default=300.0,
        help="fp: cavity length, um"
    )

    p.add_argument(
        "--group-index",
        type=float,
        default=3.6,
        help="fp: group index"
    )

    p.add_argument(
        "--mode-spacing",
        type=float,
        default=None,
        help="fp: override mode spacing, nm"
    )

    p.add_argument(
        "--mode-width",
        type=float,
        default=0.05,
        help="fp: width of each mode, nm"
    )

    # DFB
    p.add_argument(
        "--smsr",
        type=float,
        default=40.0,
        help="dfb: side-mode suppression, dB"
    )

    p.add_argument(
        "--stopband",
        type=float,
        default=3.0,
        help="dfb: stopband width, nm"
    )

    p.add_argument(
        "--side-asym",
        type=float,
        default=6.0,
        help="dfb: extra suppression of the short-wavelength side mode, dB"
    )

    # ASE pedestal
    p.add_argument(
        "--ase-db",
        type=float,
        default=None,
        help="add ASE pedestal this many dB below peak (off by default)"
    )

    p.add_argument(
        "--ase-fwhm",
        type=float,
        default=40.0,
        help="ASE pedestal FWHM, nm"
    )

    # Grid
    p.add_argument(
        "--step",
        type=float,
        default=None,
        help="grid step, nm (auto)"
    )

    p.add_argument(
        "--window",
        type=float,
        default=None,
        help="total grid width, nm (auto)"
    )

    p.add_argument(
        "--plot",
        action="store_true",
        help="also save a PNG preview"
    )

    a = p.parse_args()

    lam_nm, I = build(a)
    lam_m = lam_nm * 1e-9

    I = I * a.power / _trapz(I, lam_m)

    title = (
        f"{a.model} laser {a.wavelength:g} nm, "
        f"FWHM {a.fwhm:g} nm, {a.power:g} W/m2"
    )

    os.makedirs(a.outdir, exist_ok=True)

    csv_path = os.path.join(a.outdir, "spectra.csv")

    write_oghma(csv_path, lam_m, I, title)

    with open(os.path.join(a.outdir, "data.json"), "w") as f:
        f.write(DATA_JSON)

    print(f"wrote {a.outdir}/data.json")
    print(
        f"wrote {csv_path}: {len(lam_m)} points, "
        f"{lam_nm[0]:.3f}-{lam_nm[-1]:.3f} nm"
    )

    print(
        f"  integral    = {_trapz(I, lam_m):.6g} W/m^2"
    )

    print(
        f"  peak        = {lam_nm[np.argmax(I)]:.4f} nm, "
        f"{I.max():.4e} W/m^2/m"
    )

    if a.model == "fp":
        print(
            f"  mode spacing= {mode_spacing_nm(a):.4f} nm"
        )
    else:
        print(
            f"  meas. FWHM  = {measured_fwhm(lam_nm, I):.4f} nm"
        )

    if a.plot:
        png = os.path.join(a.outdir, "spectra.png")
        plot(png, lam_nm, I)
        print(f"  plot        = {png}")


if __name__ == "__main__":
    main()

Generowanie widm emisji diod LED za pomocą języka Python.

Oprócz widm laserowych i słonecznych, OghmaNano obsługuje również niezależnie generowane widma oświetlenia LED. Poniższy skrypt w języku Python generuje 20 reprezentatywnych źródeł światła LED na bazie półprzewodników, obejmujących emisję w zakresie ultrafioletu, światła widzialnego i podczerwieni, od 275 nm do 1550 nm.

Przykłady obejmują diody LED emitujące promieniowanie ultrafioletowe wykonane z AlGaN, niebieskie i zielone diody LED wykonane z InGaN/GaN, bursztynowe i czerwone diody LED wykonane z AlGaInP oraz diody LED emitujące promieniowanie podczerwone wykonane z GaAs, AlGaAs, InGaAs i InGaAsP. Widma generowane są na podstawie równań fenomenologicznych, a nie poprzez kopiowanie lub dopasowywanie do danych pomiarowych diod LED dostępnych na rynku. Długości fal szczytowych, szerokości linii i moce optyczne są jedynie przykładami parametrów, a nie specyfikacjami konkretnych produktów.

Fenomenologiczny model emisji

W większości przypadków stosuje się uproszczony model linii widmowej półprzewodnika o bezpośredniej przerwie energetycznej:

\[ S(E) = A\sqrt{E-E_g}\exp\left(-\frac{E-E_g}{k_BT}\right),\qquad E\geq E_g \]

W powyższym równaniu \(E\) oznacza energię fotonu, \(E_g\) – efektywną szerokość pasma emisyjnego, \(k_B\) – stałą Boltzmanna, a \(T\) – temperaturę. Przed przekształceniem w widmo długości fali stosuje się rozszerzenie Gaussa w zakresie energii fotonów. W rezultacie uzyskuje się nieznacznie asymetryczne profile emisji, a nie idealne krzywe Gaussa. W wybranych przykładach emisji zlokalizowanej, w tym dla GaP:N i GaAs:Si, stosuje się odrębny, fenomenologiczny model kształtu linii.

Normalizacja mocy optycznej i widma

Każdemu z przykładów przypisana jest przykładowa wartość mocy optycznej. W celu zapewnienia spójnego odniesienia dla oświetlenia, diodę LED przybliża się jako punktowe źródło światła o charakterystyce Lambertowskiej, obserwowane na osi w odległości 1 cm. Obliczana jest wartość promieniowania padającego:

\[ I_{\mathrm{LED}} = \frac{P_{\mathrm{opt}}}{\pi r^2},\qquad r=0.01\ \mathrm{m} \]

To jest przybliżona wartość referencyjna: rzeczywista wartość natężenia promieniowania zależy od optyki modułu LED, rozmiaru emitera, jego orientacji i odległości. Generator normalizuje każdą krzywą widmową tak, aby:

\[ \int I_{\lambda}(\lambda)\,d\lambda=I_{\mathrm{LED}} \]

OghmaNano przechowuje dane o długości fali w metrach oraz o widmowej mocy promieniowania w jednostkach W m−2 m−1. Całkowanie wygenerowanej wartości spectra.csv w funkcji długości fali pozwala na określenie wybranej wartości promieniowania padającego, wyrażonej w jednostkach W m−2.

Tworzenie biblioteki diod LED.

Zapisz poniższy skrypt pod nazwą oghma_led_material_spectra_generator.py. Wymaga on zainstalowania Pythona w wersji 3 oraz biblioteki NumPy. Uruchom go za pomocą:

python3 oghma_led_material_spectra_generator.py

Skrypt tworzy led_material_spectra_20/, czyli katalogi zawierające dane dotyczące długości fal, takie jak 450nm/, 630nm/ i 870nm/. Każdy katalog długości fal zawiera standardowe pliki data.json i spectra.csv. Wynik zawiera również katalog i listę opisujące przykłady.

Przykład dotyczący długości fali 870 nm przedstawia diodę LED z podwójną strukturą heterospójeniową GaAs/Al0,3Ga0,7As. Jej warstwy, od góry do dołu, to: 200 nm p-Al0,3Ga0,7As (domieszkowanie akceptorowe 5 × 1023 m−3), 300 nm GaAs, 200 nm n-Al0,3Ga0,7As (domieszkowanie donorowe 5 × 1023 m−3) i 100 nm n+ GaAs (domieszkowanie donorowe 1024 m−3). Całkowita grubość wynosi 800 nm. Katalog zawiera device_structure.json i DEVICE_STRUCTURE.txt; opisują one strukturę warstw, a nie pełną symulację elektryczną.

Aby wykorzystać wygenerowane źródła światła, skopiuj katalogi długości fal do folderu oghma_local/spectra/. Następnie można je wybrać w bazie danych optycznych programu OghmaNano i skalować za pomocą ustawienia „Natężenie światła (w jednostkach słonecznych)”, bez zmiany ich spektralnych charakterystyk.

🐍 Wyświetl kompletny generator spektrum LED w języku Python.
#!/usr/bin/env python3
"""Generate an independent library of phenomenological LED emission spectra.

The output is intended for OghmaNano illumination examples. Spectra are not
manufacturer measurements or fits to specific commercial devices.

A specified, illustrative external optical power (mW) is converted into the
on-axis irradiance of a point-like Lambertian LED, 1 cm from its emitting face:

    E [W/m^2] = (P_mW * 1e-3) / (pi * distance_m**2)

Each spectra.csv stores wavelength (metres) and spectral irradiance (W/m^3).
Trapezoidal integration over wavelength in metres returns E in W/m^2.

Dependencies: numpy (no SciPy needed).
Usage: python3 oghma_led_material_spectra_generator.py [output_directory]
"""

import csv
import json
import math
import sys
from pathlib import Path

import numpy as np

DISTANCE_M = 0.01
TEMPERATURE_K = 300.0
K_B_EV_K = 8.617333262145e-5
HC_EV_NM = 1239.841984

# The wavelengths, FWHMs, and powers are selected independently as useful
# representative examples of material families. They are ILLUSTRATIVE, not
# measurements or specifications of particular commercial LED devices.
# 'band_edge' is an asymmetric, broadened bulk-like phenomenological curve.
# 'localized' is a mildly asymmetric broadened emission-centre curve, used for
# GaP:N and GaAs:Si, which are not well represented by direct-gap band-edge EL.
LEDS = [
    # peak_nm, material,             emission label,             FWHM_nm, power_mW, shape
    (275,  'AlGaN',                  'UV-C',                       12,      0.2, 'band_edge'),
    (310,  'AlGaN',                  'UV-B',                       14,      0.4, 'band_edge'),
    (365,  'GaN/AlGaN',              'Near ultraviolet',           13,      2.0, 'band_edge'),
    (405,  'InGaN/GaN',              'Violet',                     17,      4.0, 'band_edge'),
    (450,  'InGaN/GaN',              'Blue',                       22,     12.0, 'band_edge'),
    (480,  'InGaN/GaN',              'Cyan',                       27,      7.0, 'band_edge'),
    (525,  'InGaN/GaN',              'Green',                      34,      3.0, 'band_edge'),
    (565,  'GaP:N',                  'Green (isoelectronic)',      48,      0.8, 'localized'),
    (590,  'AlGaInP',                'Amber',                      22,      3.0, 'band_edge'),
    (610,  'AlGaInP',                'Orange',                     22,      5.0, 'band_edge'),
    (630,  'AlGaInP',                'Red',                        22,      8.0, 'band_edge'),
    (660,  'AlGaInP',                'Deep red',                   25,      7.0, 'band_edge'),
    (740,  'AlGaAs',                 'Far red',                    30,      5.0, 'band_edge'),
    (780,  'AlGaAs',                 'Near infrared',             34,      8.0, 'band_edge'),
    (850,  'AlGaAs/GaAs',            'Near infrared',             38,     12.0, 'band_edge'),
    (870,  'GaAs/Al0.3Ga0.7As',      'GaAs double heterostructure',40,      8.0, 'band_edge'),
    (940,  'GaAs:Si',                'Near infrared',             55,      8.0, 'localized'),
    (1050, 'InGaAs/GaAs',            'Near infrared',             65,      4.0, 'band_edge'),
    (1300, 'InGaAsP/InP',            'Short-wave infrared',      100,      1.0, 'band_edge'),
    (1550, 'InGaAsP/InP',            'Short-wave infrared',      120,      0.5, 'band_edge'),
]

# This example is the exact user-specified layer stack, top to bottom.
# It documents an LED device, not a fully parameterised OghmaNano sim.json.
GAAS_870_DEVICE = {
    'example_name': 'GaAs/AlGaAs double-heterostructure infrared LED',
    'nominal_peak_wavelength_nm': 870,
    'orientation': 'top_to_bottom',
    'device_total_thickness_nm': 800,
    'layers': [
        {
            'material': 'Al0.3Ga0.7As',
            'type': 'p',
            'thickness_nm': 200,
            'acceptor_doping_m^-3': 5e23,
            'donor_doping_m^-3': 0,
            'aluminium_fraction': 0.30,
        },
        {
            'material': 'GaAs',
            'type': 'intrinsic',
            'role': 'active_emitting_layer',
            'thickness_nm': 300,
            'acceptor_doping_m^-3': 0,
            'donor_doping_m^-3': 0,
        },
        {
            'material': 'Al0.3Ga0.7As',
            'type': 'n',
            'thickness_nm': 200,
            'acceptor_doping_m^-3': 0,
            'donor_doping_m^-3': 5e23,
            'aluminium_fraction': 0.30,
        },
        {
            'material': 'GaAs',
            'type': 'n+',
            'role': 'contact_layer',
            'thickness_nm': 100,
            'acceptor_doping_m^-3': 0,
            'donor_doping_m^-3': 1e24,
        },
    ],
    'note': 'Structure-only example; optical spectrum is phenomenological. '
            'An OghmaNano electrical simulation still needs its full model input.',
}


def wavelength_grid(peak_nm, fwhm_nm):
    """Cover each emission band with plenty of margin but no huge zero tails."""
    margin = max(130.0, 8.0 * fwhm_nm)
    lo = max(210.0, peak_nm - margin)
    hi = peak_nm + margin
    return np.arange(lo, hi + 0.125, 0.25)


def energy_profile(shape_type, gaussian_width_ev):
    """Return a spectral-power shape tabulated against relative photon energy."""
    step_ev = 0.001
    relative_ev = np.arange(-1.5, 1.5 + step_ev / 2, step_ev)

    if shape_type == 'band_edge':
        above_edge = np.maximum(relative_ev, 0.0)
        e_thermal = K_B_EV_K * TEMPERATURE_K
        ideal = np.sqrt(above_edge) * np.exp(-above_edge / e_thermal)

        # Convolution with a Gaussian in energy. Fourier convolution is fast
        # and the grid is wide enough for negligible boundary wraparound.
        frequencies = np.fft.rfftfreq(len(relative_ev), step_ev)
        gaussian_fft = np.exp(-0.5 * (2.0 * np.pi * frequencies * gaussian_width_ev) ** 2)
        profile = np.fft.irfft(np.fft.rfft(ideal) * gaussian_fft, n=len(relative_ev))
        profile = np.maximum(profile, 0.0)
    elif shape_type == 'localized':
        # A broadened impurity/isoelectronic-centre line, with a somewhat wider
        # low-photon-energy tail; this is an empirical shape, not a band solver.
        local_sigma = np.where(relative_ev < 0.0,
                               gaussian_width_ev * 1.25,
                               gaussian_width_ev * 0.90)
        profile = np.exp(-0.5 * (relative_ev / local_sigma) ** 2)
    else:
        raise ValueError(f'Unknown shape type: {shape_type}')
    return relative_ev, profile


def spectrum_at_center(wavelength_nm, center_ev, relative_ev, profile):
    """Convert energy spectral density to wavelength spectral density."""
    photon_energy_ev = HC_EV_NM / wavelength_nm
    spectral_ev = np.interp(photon_energy_ev - center_ev, relative_ev, profile,
                            left=0.0, right=0.0)
    return spectral_ev * HC_EV_NM / (wavelength_nm * wavelength_nm)


def fwhm_nm_of(y, wavelength_nm):
    indices = np.flatnonzero(y >= y.max() / 2.0)
    if not len(indices):
        raise ValueError('No valid emission band found')
    return wavelength_nm[indices[-1]] - wavelength_nm[indices[0]]


def tune_peak(peak_nm, wavelength_nm, relative_ev, profile):
    """Shift the chosen energy-domain profile until its wavelength peak matches."""
    target_ev = HC_EV_NM / peak_nm
    lo, hi = target_ev - 0.5, target_ev + 0.3
    for _ in range(20):
        center = (lo + hi) / 2.0
        y = spectrum_at_center(wavelength_nm, center, relative_ev, profile)
        measured_peak_nm = wavelength_nm[np.argmax(y)]
        if measured_peak_nm > peak_nm:
            lo = center  # larger energy shifts the peak toward shorter wavelengths
        else:
            hi = center
    return spectrum_at_center(wavelength_nm, (lo + hi) / 2.0,
                              relative_ev, profile)


def spectrum_for_peak(peak_nm, desired_fwhm_nm, shape_type):
    wavelength_nm = wavelength_grid(peak_nm, desired_fwhm_nm)
    lo, hi = 0.0005, 0.30  # Gaussian energy broadening (eV)
    for _ in range(18):
        mid = (lo + hi) / 2.0
        relative_ev, profile = energy_profile(shape_type, mid)
        y = tune_peak(peak_nm, wavelength_nm, relative_ev, profile)
        if fwhm_nm_of(y, wavelength_nm) < desired_fwhm_nm:
            lo = mid
        else:
            hi = mid
    relative_ev, profile = energy_profile(shape_type, (lo + hi) / 2.0)
    y = tune_peak(peak_nm, wavelength_nm, relative_ev, profile)
    return wavelength_nm, y


def on_axis_irradiance_w_m2(external_power_mw, distance_m=DISTANCE_M):
    if distance_m <= 0.0:
        raise ValueError('distance must be positive')
    return external_power_mw * 1e-3 / (math.pi * distance_m * distance_m)


def write_oghma_csv(path, title, wavelength_m, spectral_w_m3):
    header = {
        'title': title, 'type': 'xy', 'y_label': 'Wavelength',
        'data_label': 'Intensity', 'y_units': 'nm', 'y_mul': 1e9,
        'data_units': 'm^{-1}.W.m(^-2)', 'icon': 'spectra_file',
        'time ': 0.0, 'Vexternal': 0.0,
        'x_len': 1, 'y_len': len(wavelength_m), 'z_len': 1,
        'cols': 'yd',
    }
    with path.open('w', encoding='utf-8') as out:
        out.write('#oghma_csv ' + json.dumps(header, separators=(',', ':')) + '*\n')
        for wl, value in zip(wavelength_m, spectral_w_m3):
            out.write(f'{wl:.12e}\t{value:.12e}\n')


def write_data_json(path):
    # Preserve the supplied OghmaNano spectra-item metadata format.
    metadata = {
        'item_type': 'spectra', 'status': 'public', 'changelog': '',
        'spectra_import': {
            'import_file_path': 'none', 'import_x_combo_pos': 9,
            'import_data_combo_pos': 5, 'import_x_spin': 0,
            'import_data_spin': 2, 'import_title': 'Voltage - J',
            'import_xlabel': 'Voltage', 'import_data_label': 'J',
            'import_area': 0.104, 'import_data_invert': 'False',
            'import_x_invert': 'False', 'data_file': 'none',
        },
    }
    path.write_text(json.dumps(metadata, indent='\t') + '\n', encoding='utf-8')


def generate(root):
    root.mkdir(parents=True, exist_ok=True)
    manifest = []
    wavelengths_seen = set()

    for peak_nm, material, label, width_nm, power_mw, shape_type in LEDS:
        if peak_nm in wavelengths_seen:
            raise ValueError(f'Duplicate peak wavelength/folder: {peak_nm}nm')
        wavelengths_seen.add(peak_nm)

        folder = root / f'{peak_nm}nm'
        folder.mkdir(parents=True, exist_ok=True)
        wave_nm, shape = spectrum_for_peak(peak_nm, width_nm, shape_type)
        wave_m = wave_nm * 1e-9
        area = np.trapezoid(shape, wave_m) if hasattr(np, 'trapezoid') else np.trapz(shape, wave_m)
        if area <= 0.0:
            raise ValueError(f'Unable to normalise {peak_nm} nm spectrum')
        target_w_m2 = on_axis_irradiance_w_m2(power_mw)
        spectral_w_m3 = shape * (target_w_m2 / area)
        actual_area = (np.trapezoid(spectral_w_m3, wave_m)
                       if hasattr(np, 'trapezoid') else np.trapz(spectral_w_m3, wave_m))

        title = (f'{material} {label} LED {peak_nm} nm '
                 f'at {DISTANCE_M * 100:g} cm {target_w_m2:.4f} Wm^{{-2}}')
        write_oghma_csv(folder / 'spectra.csv', title, wave_m, spectral_w_m3)
        write_data_json(folder / 'data.json')

        if peak_nm == 870:
            (folder / 'device_structure.json').write_text(
                json.dumps(GAAS_870_DEVICE, indent=2) + '\n', encoding='utf-8')
            (folder / 'DEVICE_STRUCTURE.txt').write_text(
                'GaAs/AlGaAs double-heterostructure infrared LED (~870 nm)\n'
                'Top to bottom:\n'
                '1. p-type Al0.3Ga0.7As, 200 nm, NA = 5e23 m^-3, Al = 0.30\n'
                '2. Intrinsic GaAs (emitting active layer), 300 nm, undoped\n'
                '3. n-type Al0.3Ga0.7As, 200 nm, ND = 5e23 m^-3, Al = 0.30\n'
                '4. n+ GaAs (contact layer), 100 nm, ND = 1e24 m^-3\n'
                'Total thickness: 800 nm.\n'
                'This is a layer specification, not a complete OghmaNano sim.json.\n',
                encoding='utf-8',
            )

        manifest.append({
            'folder': folder.name, 'peak_wavelength_nm': peak_nm,
            'material_system': material, 'emission': label,
            'nominal_fwhm_nm': width_nm,
            'lineshape': shape_type,
            'assumed_external_optical_power_mw': power_mw,
            'reference_distance_m': DISTANCE_M,
            'angular_emission_assumption': 'point Lambertian, forward hemisphere',
            'integrated_irradiance_w_m2': float(actual_area),
            'device_structure_file': 'device_structure.json' if peak_nm == 870 else None,
        })
        print(f'{peak_nm:4d} nm  {material:20s} {power_mw:5.1f} mW'
              f' -> {actual_area:8.3f} W/m^2'
              f'  FWHM {fwhm_nm_of(shape, wave_nm):6.1f} nm')

    (root / 'manifest.json').write_text(json.dumps(manifest, indent=2) + '\n', encoding='utf-8')

    with (root / 'catalogue.csv').open('w', newline='', encoding='utf-8') as out:
        writer = csv.writer(out)
        writer.writerow(['folder', 'material_system', 'emission', 'peak_nm',
                         'fwhm_nm', 'external_power_mw', 'irradiance_at_1cm_w_m2', 'shape'])
        for m in manifest:
            writer.writerow([m['folder'], m['material_system'], m['emission'],
                             m['peak_wavelength_nm'], m['nominal_fwhm_nm'],
                             m['assumed_external_optical_power_mw'],
                             f"{m['integrated_irradiance_w_m2']:.8f}", m['lineshape']])

    (root / 'README.md').write_text(
        '# Independent phenomenological LED emission examples\n\n'
        '20 representative material systems / emitting bands from 275 to 1550 nm.\n'
        'These spectra are analytically generated, not copied or fitted from any\n'
        'manufacturer spectrum. Chosen wavelengths, FWHMs and powers are\n'
        'illustrative example operating points, not device specifications.\n\n'
        'Each `xxxnm/` contains `spectra.csv` and `data.json`.\n'
        '`870nm/` additionally contains the specified 800 nm-thick GaAs/AlGaAs\n'
        'double-heterostructure device layer stack. That JSON describes its\n'
        'layers and doping, not a complete electrical simulation input.\n\n'
        'The spectral file uses wavelength in **metres** and spectral irradiance\n'
        'in **W/m^3**. Numerically integrating with respect to wavelength in\n'
        'metres gives irradiance in **W/m^2**.\n\n'
        '## Reference geometry\n\n'
        'A point Lambertian emitter radiates its assumed external optical power\n'
        'into the forward hemisphere. At 1 cm directly on-axis:\n\n'
        '`E [W/m^2] = P_external [W] / (pi * (0.01 m)^2)`.\n\n'
        'This ignores packaging optics, near-field geometry, finite chip size,\n'
        'source orientation, drive current and temperature dependence. In\n'
        'particular, edge-emitting infrared sources may not be Lambertian;\n'
        'these spectra are example incident-source conventions only.\n\n'
        '## Phenomenological lineshapes\n\n'
        'Most bands: `(E-Eg)^0.5 exp[-(E-Eg)/kBT]` above the effective edge,\n'
        'Gaussian-broadened in photon energy and converted to wavelength.\n'
        'The GaP:N and GaAs:Si bands use a phenomenological broadened, mildly\n'
        'asymmetric localized-emission-centre shape instead. Width is tuned\n'
        'numerically to the per-example target FWHM and all curves are scaled\n'
        'to the listed irradiance. These are not predictions from alloy\n'
        'composition or band-structure calculations.\n',
        encoding='utf-8',
    )


if __name__ == '__main__':
    generate(Path(sys.argv[1]) if len(sys.argv) > 1 else Path('led_material_spectra_20'))