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.
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.
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 (??).
data.json, spectra.csv oraz opcjonalny plik bibliograficzny.
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.
- Prasa Wygraj+Rtyp
%USERPROFILE%\oghma_local\spectrai naciśnij Wejdź/Wpisz. - 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).
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'))