Home Esempi Immagini dello schermo Manuale utente Logo di Bluesky YouTube ☰
OghmaNano Piattaforma di simulazione multifisica per dispositivi optoelettronici e sistemi fotonici SCARICA Guida rapida per iniziare.

Database di spettri otticiDatabase di spettri ottici

PanoramicaPanoramica

Il database ottico di OghmaNano memorizza spettri relativi a sorgenti di luce realistiche, tra cui la luce solare, i laser e gli spettri di illuminazione misurati sperimentalmente. Ogni spettro descrive come varia l’intensità di una sorgente di luce in funzione della lunghezza d’onda. Questi dati vengono in genere memorizzati in due colonne: lunghezza d’onda in metri (m) e irradianza spettrale in W m−2 m−1. Integrando lo spettro rispetto alla lunghezza d’onda, si ottiene la potenza ottica totale incidente per unità di area, misurata in W m−2.Il database ottico di OghmaNano memorizza spettri relativi a sorgenti di luce realistiche, tra cui la luce solare, i laser e gli spettri di illuminazione misurati sperimentalmente. Ogni spettro descrive come varia l’intensità di una sorgente di luce in funzione della lunghezza d’onda. Questi dati vengono in genere memorizzati in due colonne: lunghezza d’onda in metri (m) e irradianza spettrale in W m−2 m−1. Integrando lo spettro rispetto alla lunghezza d’onda, si ottiene la potenza ottica totale incidente per unità di area, misurata in W m−2.

Quando viene selezionato uno spettro come sorgente di illuminazione, OghmaNano lo utilizza per calcolare la generazione ottica all’interno del dispositivo, in funzione della lunghezza d’onda. L’impostazione dell’intensità luminosa (in unità solari) funge da moltiplicatore per lo spettro selezionato. Un valore di 1,0 utilizza l’intensità spettrale originale, 0,5 la riduce della metà e 0,0 disattiva l’illuminazione. In questo modo, è possibile utilizzare lo stesso spettro della sorgente luminosa con diverse intensità di illuminazione, senza modificare i dati di base.Quando viene selezionato uno spettro come sorgente di illuminazione, OghmaNano lo utilizza per calcolare la generazione ottica all’interno del dispositivo, in funzione della lunghezza d’onda. L’impostazione dell’intensità luminosa (in unità solari) funge da moltiplicatore per lo spettro selezionato. Un valore di 1,0 utilizza l’intensità spettrale originale, 0,5 la riduce della metà e 0,0 disattiva l’illuminazione. In questo modo, è possibile utilizzare lo stesso spettro della sorgente luminosa con diverse intensità di illuminazione, senza modificare i dati di base.

Visualizzazione del database otticoVisualizzazione del database ottico

Per visualizzare gli spettri disponibili, fare clic sull’icona del database ottico nella barra multifunzione Ottica (??). Si aprirà il browser del database ottico (??), dove sarà possibile visualizzare, importare e selezionare gli spettri delle sorgenti luminose per utilizzarli nelle simulazioni.Per visualizzare gli spettri disponibili, fare clic sull’icona del database ottico nella barra multifunzione Ottica (??). Si aprirà il browser del database ottico (??), dove sarà possibile visualizzare, importare e selezionare gli spettri delle sorgenti luminose per utilizzarli nelle simulazioni.

OghmaNano Databases ribbon with the Optical database icon highlighted.
Nella barra multifunzione «Database» di OghmaNano, selezionando il database «Ottico» si apre il database degli spettri ottici.Nella barra multifunzione «Database» di OghmaNano, selezionando il database «Ottico» si apre il database degli spettri ottici.
OghmaNano window showing the Optical database directory with available spectra entries.
La directory del database ottico elenca gli spettri disponibili. Facendo doppio clic su una voce, i relativi dati vengono aperti nell’editor.La directory del database ottico elenca gli spettri disponibili. Facendo doppio clic su una voce, i relativi dati vengono aperti nell’editor.

Facendo doppio clic su uno spettro, questo si apre nell’editor di spettri ottici (??), dove è possibile visualizzare i dati esistenti o importare nuovi spettri. La scheda «Base» (??) contiene campi per note, metadati e opzioni relative alla privacy.Facendo doppio clic su uno spettro, questo si apre nell’editor di spettri ottici (??), dove è possibile visualizzare i dati esistenti o importare nuovi spettri. La scheda «Base» (??) contiene campi per note, metadati e opzioni relative alla privacy.

OghmaNano optical spectrum editor showing a solar spectrum plot of spectral irradiance versus wavelength.
La scheda «Spettri» nell’editor dello spettro ottico visualizza l’irradianza spettrale solare in funzione della lunghezza d’onda.La scheda «Spettri» nell’editor dello spettro ottico visualizza l’irradianza spettrale solare in funzione della lunghezza d’onda.
OghmaNano optical spectrum editor showing the Basic tab with metadata fields.
La scheda «Elementi di base» dell’editor dello spettro ottico. Utilizzatela per aggiungere note, impostare le opzioni di privacy e fornire metadati descrittivi per lo spettro.La scheda «Elementi di base» dell’editor dello spettro ottico. Utilizzatela per aggiungere note, impostare le opzioni di privacy e fornire metadati descrittivi per lo spettro.

Importazione di spettri otticiImportazione di spettri ottici

OghmaNano consente agli utenti di importare i propri spettri di sorgenti luminose, inclusi gli spettri solari misurati, i profili di emissione laser e altri dati sperimentali. Per importare uno spettro, aprire l'editor di spettri ottici e fare clic su «Da file». Si aprirà la finestra di importazione dati (??). Selezionare il file desiderato utilizzando l'opzione «Apri file dati», quindi identificare le colonne relative alla lunghezza d'onda e all'intensità e specificare le rispettive unità di misura. Ad esempio, le lunghezze d'onda possono essere indicate in nanometri anziché in metri. OghmaNano converte automaticamente i valori importati nelle unità di misura del Sistema Internazionale. Fare clic su «Importa dati» per caricare lo spettro convertito nell'editor.OghmaNano consente agli utenti di importare i propri spettri di sorgenti luminose, inclusi gli spettri solari misurati, i profili di emissione laser e altri dati sperimentali. Per importare uno spettro, aprire l'editor di spettri ottici e fare clic su «Da file». Si aprirà la finestra di importazione dati (??). Selezionare il file desiderato utilizzando l'opzione «Apri file dati», quindi identificare le colonne relative alla lunghezza d'onda e all'intensità e specificare le rispettive unità di misura. Ad esempio, le lunghezze d'onda possono essere indicate in nanometri anziché in metri. OghmaNano converte automaticamente i valori importati nelle unità di misura del Sistema Internazionale. Fare clic su «Importa dati» per caricare lo spettro convertito nell'editor.

Gli spettri importati vengono salvati nel database ottico locale nella cartella oghma_local/spectra/. Ogni spettro ha una propria cartella, come illustrato in ??. Lo spettro stesso viene memorizzato in spectra.csv, che può anche essere aperto direttamente in un editor di testo (??).Gli spettri importati vengono salvati nel database ottico locale nella cartella oghma_local/spectra/. Ogni spettro ha una propria cartella, come illustrato in ??. Lo spettro stesso viene memorizzato in spectra.csv, che può anche essere aperto direttamente in un editor di testo (??).

OghmaNano data import window showing wavelength and intensity data being converted into SI units.
La finestra di importazione dati mostra i dati di input originali e i valori corrispondenti convertiti nelle unità di misura del Sistema Internazionale.La finestra di importazione dati mostra i dati di input originali e i valori corrispondenti convertiti nelle unità di misura del Sistema Internazionale.
File browser showing an OghmaNano optical spectrum directory containing JSON, CSV and bibliography files.
Una directory che contiene una serie di file, tra cui data.json, spectra.csv e, facoltativamente, un file bibliografico.Una directory che contiene una serie di file, tra cui data.json, spectra.csv e, facoltativamente, un file bibliografico.
Text editor displaying an OghmaNano spectra.csv file with its header and wavelength-intensity data.
Il contenuto di spectra.csv mostra l’intestazione del file e l’irradianza spettrale in funzione della lunghezza d’onda.Il contenuto di spectra.csv mostra l’intestazione del file e l’irradianza spettrale in funzione della lunghezza d’onda.

Come vengono memorizzati gli spettri delle sorgenti luminose.Come vengono memorizzati gli spettri delle sorgenti luminose.

Ogni spettro presente nel database ottico OghmaNano viene memorizzato in una propria directory, che contiene due file principali: data.json e spectra.csv. È possibile includere anche un file facoltativo spectra.bib per fornire riferimenti bibliografici. Il file data.json identifica la directory come spettro di una sorgente luminosa, utilizzando il campo "item_type":"spectra". I dati spettrali numerici vengono memorizzati separatamente in spectra.csv.Ogni spettro presente nel database ottico OghmaNano viene memorizzato in una propria directory, che contiene due file principali: data.json e spectra.csv. È possibile includere anche un file facoltativo spectra.bib per fornire riferimenti bibliografici. Il file data.json identifica la directory come spettro di una sorgente luminosa, utilizzando il campo "item_type":"spectra". I dati spettrali numerici vengono memorizzati separatamente in spectra.csv.

Il file spectra.csv inizia con un #oghma_csv header che descrive i dati, seguito da due colonne di valori numerici. La prima colonna contiene la lunghezza d’onda in metri (m), mentre la seconda contiene l’irradianza spettrale in W m−2 m−1. Sebbene le lunghezze d’onda siano memorizzate in metri, il campo y_mul nell’header consente a OghmaNano di visualizzarle in nanometri. L’esempio seguente mostra l’inizio dello spettro solare AM1.5G:Il file spectra.csv inizia con un #oghma_csv header che descrive i dati, seguito da due colonne di valori numerici. La prima colonna contiene la lunghezza d’onda in metri (m), mentre la seconda contiene l’irradianza spettrale in W m−2 m−1. Sebbene le lunghezze d’onda siano memorizzate in metri, il campo y_mul nell’header consente a OghmaNano di visualizzarle in nanometri. L’esempio seguente mostra l’inizio dello spettro solare 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
...

Un campo importante nell’intestazione è y_len, che indica il numero di punti dati relativi alla lunghezza d’onda e all’intensità. Questo valore deve corrispondere al numero di righe numeriche presenti nel file. Nell’esempio precedente, y_len è 2002 perché lo spettro completo AM1.5G contiene 2002 punti dati.Un campo importante nell’intestazione è y_len, che indica il numero di punti dati relativi alla lunghezza d’onda e all’intensità. Questo valore deve corrispondere al numero di righe numeriche presenti nel file. Nell’esempio precedente, y_len è 2002 perché lo spettro completo AM1.5G contiene 2002 punti dati.

L’area sottesa dalla curva dell’irradianza spettrale rappresenta la potenza ottica totale incidente per unità di superficie:L’area sottesa dalla curva dell’irradianza spettrale rappresenta la potenza ottica totale incidente per unità di superficie:

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

In questo caso, \(I_{\lambda}\) rappresenta l’irradianza spettrale, espressa in W m−2 m−1, e la densità di potenza risultante viene misurata in W m−2. Ad esempio, lo spettro solare standard AM1.5G, integrato, raggiunge un valore di circa 1000 W m−2. È importante che gli spettri importati presentino l’intensità integrata corretta, poiché OghmaNano utilizza direttamente i valori contenuti nel file, anziché normalizzarli automaticamente.In questo caso, \(I_{\lambda}\) rappresenta l’irradianza spettrale, espressa in W m−2 m−1, e la densità di potenza risultante viene misurata in W m−2. Ad esempio, lo spettro solare standard AM1.5G, integrato, raggiunge un valore di circa 1000 W m−2. È importante che gli spettri importati presentino l’intensità integrata corretta, poiché OghmaNano utilizza direttamente i valori contenuti nel file, anziché normalizzarli automaticamente.

Gli utenti possono anche creare manualmente i propri spettri, senza utilizzare lo strumento di importazione. Il metodo più semplice consiste nel copiare una cartella contenente uno spettro esistente, sostituire i valori di lunghezza d’onda e intensità nel file spectra.csv e aggiornare le informazioni nell’intestazione, in particolare y_len. Il file data.json può essere mantenuto dalla cartella originale. In questo modo è possibile definire in modo semplice spettri laser personalizzati, sorgenti di illuminazione monocromatica o sorgenti luminose misurate sperimentalmente.Gli utenti possono anche creare manualmente i propri spettri, senza utilizzare lo strumento di importazione. Il metodo più semplice consiste nel copiare una cartella contenente uno spettro esistente, sostituire i valori di lunghezza d’onda e intensità nel file spectra.csv e aggiornare le informazioni nell’intestazione, in particolare y_len. Il file data.json può essere mantenuto dalla cartella originale. In questo modo è possibile definire in modo semplice spettri laser personalizzati, sorgenti di illuminazione monocromatica o sorgenti luminose misurate sperimentalmente.

Windows: apri rapidamente la cartella locale contenente i tuoi dati spettrali.Windows: apri rapidamente la cartella locale contenente i tuoi dati spettrali.

  1. Stampa Vincere+R, tipo %USERPROFILE%\oghma_local\spectrae premi Inserisci.Stampa Vincere+R, tipo %USERPROFILE%\oghma_local\spectrae premi Inserisci.
  2. Ogni spettro viene salvato in una sottocartella dedicata (ad esempio, ...\spectra\example\).Ogni spettro viene salvato in una sottocartella dedicata (ad esempio, ...\spectra\example\).

Generatore di spettro solareGeneratore di spettro solare

Il generatore dello spettro solare (??) può essere avviato dall’editor dello spettro ottico. Esso confronta lo spettro di riferimento AM1.5G con le componenti calcolate di irradianza globale (Iglobal), irradianza diretta (Idirect) e irradianza diffusa (Idiffuse), e consente di generare spettri per diversi momenti della giornata, diverse località geografiche e diverse condizioni atmosferiche (pressione, umidità e concentrazione di aerosol).Il generatore dello spettro solare (??) può essere avviato dall’editor dello spettro ottico. Esso confronta lo spettro di riferimento AM1.5G con le componenti calcolate di irradianza globale (Iglobal), irradianza diretta (Idirect) e irradianza diffusa (Idiffuse), e consente di generare spettri per diversi momenti della giornata, diverse località geografiche e diverse condizioni atmosferiche (pressione, umidità e concentrazione di aerosol).

Il generatore si basa sul modello semplificato dello spettro solare per l’irradiazione diretta e diffusa su piani orizzontali e inclinati sulla superficie terrestre, in condizioni di atmosfera serena, sviluppato da Bird e Riordan (1986) e pubblicato sulla rivista «Journal of Applied Meteorology and Climatology». È possibile consultare l’articolo originale qui. Questo modello, in parte empirico, calcola l’irradiazione spettrale solare sulla superficie terrestre, tenendo conto dei principali processi atmosferici, come la diffusione di Rayleigh, l’assorbimento dell’ozono, la presenza di gas uniformemente distribuiti, l’assorbimento del vapore acqueo e l’estinzione degli aerosol. Utilizzando dati di input quali la posizione geografica, l’ora del giorno, l’altitudine e i parametri atmosferici, il modello suddivide lo spettro solare nelle componenti globale, diretta e diffusa. Viene fornito il riferimento AM1.5G come parametro di riferimento, consentendo agli utenti di generare spettri specifici per un determinato sito e per determinate condizioni, che tengano conto sia dei fattori geometrici (angolo zenitale solare, massa d’aria) sia delle proprietà atmosferiche (pressione, vapore acqueo e profondità ottica degli aerosol).Il generatore si basa sul modello semplificato dello spettro solare per l’irradiazione diretta e diffusa su piani orizzontali e inclinati sulla superficie terrestre, in condizioni di atmosfera serena, sviluppato da Bird e Riordan (1986) e pubblicato sulla rivista «Journal of Applied Meteorology and Climatology». È possibile consultare l’articolo originale qui. Questo modello, in parte empirico, calcola l’irradiazione spettrale solare sulla superficie terrestre, tenendo conto dei principali processi atmosferici, come la diffusione di Rayleigh, l’assorbimento dell’ozono, la presenza di gas uniformemente distribuiti, l’assorbimento del vapore acqueo e l’estinzione degli aerosol. Utilizzando dati di input quali la posizione geografica, l’ora del giorno, l’altitudine e i parametri atmosferici, il modello suddivide lo spettro solare nelle componenti globale, diretta e diffusa. Viene fornito il riferimento AM1.5G come parametro di riferimento, consentendo agli utenti di generare spettri specifici per un determinato sito e per determinate condizioni, che tengano conto sia dei fattori geometrici (angolo zenitale solare, massa d’aria) sia delle proprietà atmosferiche (pressione, vapore acqueo e profondità ottica degli aerosol).

OghmaNano Solar Spectrum Generator window showing AM1.5G reference spectrum compared with calculated global, direct, and diffuse components.
Finestra del generatore dello spettro solare. Confronta lo spettro di riferimento AM1.5G con i componenti calcolati (Iglobal, Idirect, Idiffuse) e rigenera gli spettri in condizioni diverse.Finestra del generatore dello spettro solare. Confronta lo spettro di riferimento AM1.5G con i componenti calcolati (Iglobal, Idirect, Idiffuse) e rigenera gli spettri in condizioni diverse.

Creazione di spettri laser personalizzati con PythonCreazione di spettri laser personalizzati con Python

Oltre a importare spettri misurati sperimentalmente, OghmaNano consente agli utenti di generare i propri spettri di sorgenti luminose utilizzando script esterni. Ad esempio, il seguente programma Python genera spettri di emissione laser realistici e li salva direttamente nel formato del database ottico di OghmaNano. Questo è utile quando si simulano dispositivi illuminati da laser, come i fotodiodi InGaAs che operano a 1550 nm, i sensori ottici e i fotodetettori a semiconduttore.Oltre a importare spettri misurati sperimentalmente, OghmaNano consente agli utenti di generare i propri spettri di sorgenti luminose utilizzando script esterni. Ad esempio, il seguente programma Python genera spettri di emissione laser realistici e li salva direttamente nel formato del database ottico di OghmaNano. Questo è utile quando si simulano dispositivi illuminati da laser, come i fotodiodi InGaAs che operano a 1550 nm, i sensori ottici e i fotodetettori a semiconduttore.

Lo script supporta tre diversi modelli di laser: uno spettro gaussiano o lorentziano per una sorgente semplice a banda stretta, un laser Fabry–Pérot con più modalità longitudinali e un laser a retroazione distribuita (DFB) con un picco di emissione dominante e modalità secondarie più deboli. È inoltre possibile includere, facoltativamente, uno sfondo di emissione spontanea amplificata (ASE). La lunghezza d’onda centrale, la larghezza spettrale e la potenza ottica totale possono essere regolate tramite la riga di comando.Lo script supporta tre diversi modelli di laser: uno spettro gaussiano o lorentziano per una sorgente semplice a banda stretta, un laser Fabry–Pérot con più modalità longitudinali e un laser a retroazione distribuita (DFB) con un picco di emissione dominante e modalità secondarie più deboli. È inoltre possibile includere, facoltativamente, uno sfondo di emissione spontanea amplificata (ASE). La lunghezza d’onda centrale, la larghezza spettrale e la potenza ottica totale possono essere regolate tramite la riga di comando.

Per utilizzare lo script, salvare il codice riportato di seguito con il nome make_laser_spectrum.py. Richiede Python 3 e NumPy; Matplotlib è necessario solo per generare un grafico di anteprima. Eseguendolo senza argomenti, viene creata una directory denominata laser_1550nm che contiene data.json e spectra.csv. Per impostazione predefinita, lo spettro generato è centrato a 1550 nm, ha una larghezza massima a metà altezza (FWHM) gaussiana di 2 nm e una densità di potenza ottica integrata di 10 W m−2.Per utilizzare lo script, salvare il codice riportato di seguito con il nome make_laser_spectrum.py. Richiede Python 3 e NumPy; Matplotlib è necessario solo per generare un grafico di anteprima. Eseguendolo senza argomenti, viene creata una directory denominata laser_1550nm che contiene data.json e spectra.csv. Per impostazione predefinita, lo spettro generato è centrato a 1550 nm, ha una larghezza massima a metà altezza (FWHM) gaussiana di 2 nm e una densità di potenza ottica integrata di 10 W m−2.

python3 make_laser_spectrum.py

È possibile generare altri modelli di laser modificando gli argomenti della riga di comando. Ad esempio, il comando seguente genera lo spettro di un laser Fabry–Pérot con diverse modalità longitudinali e un rumore di amplificazione spontanea (ASE) inferiore di 40 dB rispetto al picco:È possibile generare altri modelli di laser modificando gli argomenti della riga di comando. Ad esempio, il comando seguente genera lo spettro di un laser Fabry–Pérot con diverse modalità longitudinali e un rumore di amplificazione spontanea (ASE) inferiore di 40 dB rispetto al picco:

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

In alternativa, è possibile ottenere un laser DFB a banda stretta utilizzando:In alternativa, è possibile ottenere un laser DFB a banda stretta utilizzando:

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

Lo spettro generato viene normalizzato automaticamente in modo che l'integrale della sua irradianza spettrale sia uguale alla densità di potenza ottica specificata. Lo script calcola anche il numero di punti di lunghezza d'onda e inserisce il valore corretto y_len nell'intestazione #oghma_csv. In questo modo è possibile generare spettri senza dover modificare manualmente nessuno dei file di output.Lo spettro generato viene normalizzato automaticamente in modo che l'integrale della sua irradianza spettrale sia uguale alla densità di potenza ottica specificata. Lo script calcola anche il numero di punti di lunghezza d'onda e inserisce il valore corretto y_len nell'intestazione #oghma_csv. In questo modo è possibile generare spettri senza dover modificare manualmente nessuno dei file di output.

Una volta generata, copiate la cartella risultante in oghma_local/spectra/. La nuova sorgente luminosa sarà quindi disponibile tramite il database ottico di OghmaNano. Poiché l’output utilizza il formato standard dello spettro di OghmaNano, è possibile utilizzare la stessa procedura per creare una libreria di sorgenti laser con diverse lunghezze d’onda e potenze.Una volta generata, copiate la cartella risultante in oghma_local/spectra/. La nuova sorgente luminosa sarà quindi disponibile tramite il database ottico di OghmaNano. Poiché l’output utilizza il formato standard dello spettro di OghmaNano, è possibile utilizzare la stessa procedura per creare una libreria di sorgenti laser con diverse lunghezze d’onda e potenze.

🐍 Mostra il codice completo per la generazione dello spettro laser in Python.🐍 Mostra il codice completo per la generazione dello spettro laser in 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()

Creazione di spettri di emissione LED con PythonCreazione di spettri di emissione LED con Python

Oltre agli spettri laser e solari, OghmaNano supporta anche spettri di illuminazione LED generati in modo indipendente. Lo script Python riportato di seguito genera 20 sorgenti LED a semiconduttore rappresentative, che coprono le emissioni ultraviolette, visibili e infrarosse da 275 nm a 1550 nm.Oltre agli spettri laser e solari, OghmaNano supporta anche spettri di illuminazione LED generati in modo indipendente. Lo script Python riportato di seguito genera 20 sorgenti LED a semiconduttore rappresentative, che coprono le emissioni ultraviolette, visibili e infrarosse da 275 nm a 1550 nm.

Gli esempi includono LED ultravioletti in AlGaN, LED blu e verdi in InGaN/GaN, LED ambra e rossi in AlGaInP e LED a infrarossi in GaAs, AlGaAs, InGaAs e InGaAsP. Gli spettri sono generati a partire da equazioni fenomenologiche, anziché essere ricavati o adattati da misurazioni di LED commerciali. Le lunghezze d’onda di picco, le larghezze di banda e le potenze ottiche sono parametri indicativi e non costituiscono le specifiche di prodotti particolari.Gli esempi includono LED ultravioletti in AlGaN, LED blu e verdi in InGaN/GaN, LED ambra e rossi in AlGaInP e LED a infrarossi in GaAs, AlGaAs, InGaAs e InGaAsP. Gli spettri sono generati a partire da equazioni fenomenologiche, anziché essere ricavati o adattati da misurazioni di LED commerciali. Le lunghezze d’onda di picco, le larghezze di banda e le potenze ottiche sono parametri indicativi e non costituiscono le specifiche di prodotti particolari.

Modello di emissione fenomenologicoModello di emissione fenomenologico

La maggior parte degli spettri utilizza una forma semplificata della curva di energia per i semiconduttori con banda proibita diretta:La maggior parte degli spettri utilizza una forma semplificata della curva di energia per i semiconduttori con banda proibita diretta:

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

In questo caso, \(E\) rappresenta l’energia del fotone, \(E_g\) è la larghezza di banda di emissione effettiva, \(k_B\) è la costante di Boltzmann e \(T\) è la temperatura. Prima della conversione in uno spettro di lunghezze d’onda, viene applicato un allargamento gaussiano all’energia del fotone. Questo produce profili di emissione leggermente asimmetrici, anziché gaussiani ideali. Alcuni esempi selezionati di emissione localizzata, tra cui GaP:N e GaAs:Si, utilizzano una funzione fenomenologica separata per descrivere la forma delle linee di emissione.In questo caso, \(E\) rappresenta l’energia del fotone, \(E_g\) è la larghezza di banda di emissione effettiva, \(k_B\) è la costante di Boltzmann e \(T\) è la temperatura. Prima della conversione in uno spettro di lunghezze d’onda, viene applicato un allargamento gaussiano all’energia del fotone. Questo produce profili di emissione leggermente asimmetrici, anziché gaussiani ideali. Alcuni esempi selezionati di emissione localizzata, tra cui GaP:N e GaAs:Si, utilizzano una funzione fenomenologica separata per descrivere la forma delle linee di emissione.

Normalizzazione della potenza ottica e dello spettroNormalizzazione della potenza ottica e dello spettro

A ciascun esempio viene assegnata una potenza ottica esterna di riferimento. Per garantire un riferimento di illuminazione uniforme, il LED viene approssimato come una sorgente puntiforme lambertiana osservata sull’asse, a una distanza di 1 cm. La sua irradianza incidente viene calcolata come segue:A ciascun esempio viene assegnata una potenza ottica esterna di riferimento. Per garantire un riferimento di illuminazione uniforme, il LED viene approssimato come una sorgente puntiforme lambertiana osservata sull’asse, a una distanza di 1 cm. La sua irradianza incidente viene calcolata come segue:

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

Si tratta di un valore di riferimento approssimativo: l’irradianza effettiva dipende dalle caratteristiche ottiche del modulo LED, dalle dimensioni dell’emettitore, dall’orientamento e dalla distanza. Il generatore normalizza ogni curva spettrale in modo che:Si tratta di un valore di riferimento approssimativo: l’irradianza effettiva dipende dalle caratteristiche ottiche del modulo LED, dalle dimensioni dell’emettitore, dall’orientamento e dalla distanza. Il generatore normalizza ogni curva spettrale in modo che:

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

OghmaNano memorizza la lunghezza d’onda in metri e l’irradianza spettrale in W m−2 m−1. Integrando il valore spectra.csv generato rispetto alla lunghezza d’onda, si ottiene quindi l’irradianza incidente selezionata, espressa in W m−2.OghmaNano memorizza la lunghezza d’onda in metri e l’irradianza spettrale in W m−2 m−1. Integrando il valore spectra.csv generato rispetto alla lunghezza d’onda, si ottiene quindi l’irradianza incidente selezionata, espressa in W m−2.

Creazione della libreria LED.Creazione della libreria LED.

Salva lo script seguente con il nome oghma_led_material_spectra_generator.py. Richiede Python 3 e NumPy. Per eseguirlo, utilizza il comando:Salva lo script seguente con il nome oghma_led_material_spectra_generator.py. Richiede Python 3 e NumPy. Per eseguirlo, utilizza il comando:

python3 oghma_led_material_spectra_generator.py

Lo script crea led_material_spectra_20/, che contiene le cartelle relative alle lunghezze d’onda, come 450nm/, 630nm/ e 870nm/. Ogni cartella relativa alle lunghezze d’onda contiene i file standard data.json e spectra.csv. L’output include anche un catalogo e un file di metadati che descrivono gli esempi.Lo script crea led_material_spectra_20/, che contiene le cartelle relative alle lunghezze d’onda, come 450nm/, 630nm/ e 870nm/. Ogni cartella relativa alle lunghezze d’onda contiene i file standard data.json e spectra.csv. L’output include anche un catalogo e un file di metadati che descrivono gli esempi.

L’esempio a 870 nm descrive un LED a doppia eterostruttura GaAs/Al0,3Ga0,7As. I suoi strati, dall’alto verso il basso, sono: 200 nm di p-Al0,3Ga0,7As (drogaggio accettore 5×1023 m−3), 300 nm di GaAs intrinseco, 200 nm di n-Al0,3Ga0,7As (drogaggio donatore 5×1023 m−3) e 100 nm di n+ GaAs (drogaggio donatore 1024 m−3). Lo spessore totale è di 800 nm. La directory include device_structure.json e DEVICE_STRUCTURE.txt; questi descrivono la sequenza degli strati piuttosto che una simulazione elettrica completa.L’esempio a 870 nm descrive un LED a doppia eterostruttura GaAs/Al0,3Ga0,7As. I suoi strati, dall’alto verso il basso, sono: 200 nm di p-Al0,3Ga0,7As (drogaggio accettore 5×1023 m−3), 300 nm di GaAs intrinseco, 200 nm di n-Al0,3Ga0,7As (drogaggio donatore 5×1023 m−3) e 100 nm di n+ GaAs (drogaggio donatore 1024 m−3). Lo spessore totale è di 800 nm. La directory include device_structure.json e DEVICE_STRUCTURE.txt; questi descrivono la sequenza degli strati piuttosto che una simulazione elettrica completa.

Per utilizzare le sorgenti luminose generate, copiare le cartelle contenenti le lunghezze d’onda nella directory oghma_local/spectra/. A questo punto, sarà possibile selezionarle nel database ottico di OghmaNano e regolarne l’intensità utilizzando l’impostazione «Intensità della luce (in unità solari)», senza modificarne le caratteristiche spettrali.Per utilizzare le sorgenti luminose generate, copiare le cartelle contenenti le lunghezze d’onda nella directory oghma_local/spectra/. A questo punto, sarà possibile selezionarle nel database ottico di OghmaNano e regolarne l’intensità utilizzando l’impostazione «Intensità della luce (in unità solari)», senza modificarne le caratteristiche spettrali.

🐍 Mostra il codice completo per generare lo spettro LED in Python.🐍 Mostra il codice completo per generare lo spettro LED in 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'))