Σπίτι Παραδείγματα Στιγμιότυπα οθόνης Εγχειρίδιο χρήστη Λογότυπο της Bluesky YouTube ☰
OghmaNano Πλατφόρμα προσομοίωσης πολλαπλών φυσικών πεδίων για οπτοηλεκτρονικές συσκευές και φωτονικά συστήματα. Λήψη Οδηγός γρήγορης εκκίνησης

Βάση Δεδομένων Οπτικών Φασμάτων

Επισκόπηση

Η Οπτική Βάση Δεδομένων στο OghmaNano αποθηκεύει φάσματα για ρεαλιστικές πηγές φωτός, συμπεριλαμβανομένου του ηλιακού φωτός, των λέιζερ και των πειραματικά μετρημένων φασμάτων φωτισμού. Κάθε φάσμα περιγράφει πώς η ένταση μιας πηγής φωτός μεταβάλλεται ανάλογα με το μήκος κύματος. Αυτά τα δεδομένα συνήθως αποθηκεύονται ως δύο στήλες: μήκος κύματος σε μέτρα (m) και φασματική ακτινοβολία σε W m−2 m−1. Η ολοκλήρωση του φάσματος ως προς το μήκος κύματος δίνει τη συνολική προσπίπτουσα οπτική ισχύ ανά μονάδα επιφάνειας, η οποία μετράται σε W m−2.

Όταν επιλέγεται ένα φάσμα ως πηγή φωτισμού, το OghmaNano το χρησιμοποιεί για να υπολογίσει την οπτική παραγωγή που εξαρτάται από το μήκος κύματος εντός της συσκευής. Η ρύθμιση "Ένταση φωτός (Suns)" λειτουργεί ως πολλαπλασιαστής για το επιλεγμένο φάσμα. Μια τιμή 1,0 χρησιμοποιεί την αρχική φασματική ένταση, η 0,5 τη μειώνει στο μισό και η 0,0 απενεργοποιεί τον φωτισμό. Αυτό επιτρέπει τη χρήση του ίδιου φάσματος πηγής φωτός σε διαφορετικές εντάσεις φωτισμού χωρίς να τροποποιούνται τα υποκείμενα δεδομένα.

Προβολή της οπτικής βάσης δεδομένων.

Για να εξερευνήσετε τα διαθέσιμα φάσματα, κάντε κλικ στο εικονίδιο της Οπτικής Βάσης Δεδομένων στην καρτέλα "Οπτικά" (??). Αυτό ανοίγει το πρόγραμμα περιήγησης της οπτικής βάσης δεδομένων (??), όπου μπορείτε να δείτε, να εισαγάγετε και να επιλέξετε φάσματα πηγών φωτός για χρήση σε προσομοιώσεις.

OghmaNano Databases ribbon with the Optical database icon highlighted.
Η καρτέλα «Βάσεις δεδομένων» στο OghmaNano. Η επιλογή της οπτικής βάσης δεδομένων ανοίγει τη βάση δεδομένων οπτικών φασμάτων.
OghmaNano window showing the Optical database directory with available spectra entries.
Ο κατάλογος της οπτικής βάσης δεδομένων, ο οποίος περιέχει μια λίστα με τα διαθέσιμα φάσματα. Κάνοντας διπλό κλικ σε μια καταχώρηση, ανοίγουν τα δεδομένα της στον επεξεργαστή.

Κάνοντας διπλό κλικ σε ένα φάσμα, αυτό ανοίγει στον Επεξεργαστή Οπτικού Φάσματος (??), όπου μπορείτε να δείτε τα υπάρχοντα δεδομένα ή να εισαγάγετε νέα φάσματα. Η καρτέλα "Βασικά" (??) παρέχει πεδία για σημειώσεις, μεταδεδομένα και επιλογές απορρήτου.

OghmaNano optical spectrum editor showing a solar spectrum plot of spectral irradiance versus wavelength.
Η καρτέλα «Φάσματα» στον Επεξεργαστή Οπτικού Φάσματος, η οποία εμφανίζει την ηλιακή φασματική ακτινοβολία ως συνάρτηση του μήκους κύματος.
OghmaNano optical spectrum editor showing the Basic tab with metadata fields.
Η καρτέλα "Βασικά" του Επεξεργαστή Οπτικού Φάσματος. Χρησιμοποιήστε την για να προσθέσετε σημειώσεις, να ορίσετε τις ρυθμίσεις απορρήτου και να παρέχετε περιγραφικά μεταδεδομένα για το φάσμα.

Εισαγωγή οπτικών φασμάτων.

Το OghmaNano επιτρέπει στους χρήστες να εισάγουν τα δικά τους φάσματα πηγών φωτός, συμπεριλαμβανομένων μετρημένων ηλιακών φασμάτων, προφίλ εκπομπής λέιζερ και άλλων πειραματικών δεδομένων. Για να εισαγάγετε ένα φάσμα, ανοίξτε τον Επεξεργαστή Οπτικού Φάσματος και κάντε κλικ στην επιλογή "Από Αρχείο". Αυτό ανοίγει το παράθυρο εισαγωγής δεδομένων (??). Επιλέξτε το αρχείο σας χρησιμοποιώντας την επιλογή "Άνοιγμα αρχείου δεδομένων", στη συνέχεια προσδιορίστε τις στήλες μήκους κύματος και έντασης και καθορίστε τις μονάδες τους. Για παράδειγμα, τα μήκη κύματος μπορεί να παρέχονται σε νανόμετρα αντί για μέτρα. Το OghmaNano μετατρέπει αυτόματα τις εισαγόμενες τιμές σε μονάδες SI. Κάντε κλικ στην επιλογή "Εισαγωγή δεδομένων" για να φορτώσετε το μετατραπέν φάσμα στον επεξεργαστή.

Τα εισαγόμενα φάσματα αποθηκεύονται στην τοπική οπτική βάση δεδομένων κάτω από το oghma_local/spectra/. Κάθε φάσμα έχει τον δικό του φάκελο, όπως απεικονίζεται στο ??. Το ίδιο το φάσμα αποθηκεύεται στο spectra.csv, το οποίο μπορεί επίσης να ανοιχτεί απευθείας σε ένα πρόγραμμα επεξεργασίας κειμένου (??).

OghmaNano data import window showing wavelength and intensity data being converted into SI units.
Το παράθυρο Εισαγωγής Δεδομένων, το οποίο εμφανίζει τα ακατέργαστα δεδομένα εισόδου και τις αντίστοιχες τιμές που έχουν μετατραπεί σε μονάδες SI.
File browser showing an OghmaNano optical spectrum directory containing JSON, CSV and bibliography files.
Ένας κατάλογος φάσματος που περιέχει τα data.json, spectra.csv και ένα προαιρετικό αρχείο βιβλιογραφίας.
Text editor displaying an OghmaNano spectra.csv file with its header and wavelength-intensity data.
Το περιεχόμενο του spectra.csv περιλαμβάνει την κεφαλίδα του αρχείου και την φασματική ακτινοβολία ως συνάρτηση του μήκους κύματος.

Πώς αποθηκεύονται τα φάσματα των πηγών φωτός.

Κάθε φάσμα στην οπτική βάση δεδομένων OghmaNano αποθηκεύεται σε έναν ξεχωριστό φάκελο που περιέχει δύο κύρια αρχεία: data.json και spectra.csv. Ένα προαιρετικό αρχείο spectra.bib μπορεί επίσης να συμπεριληφθεί για να παρέχει βιβλιογραφικές αναφορές. Το αρχείο data.json προσδιορίζει τον φάκελο ως φάσμα πηγής φωτός χρησιμοποιώντας το πεδίο "item_type":"spectra". Τα αριθμητικά φασματικά δεδομένα αποθηκεύονται ξεχωριστά στο spectra.csv.

Το αρχείο spectra.csv ξεκινά με μια κεφαλίδα #oghma_csv που περιγράφει τα δεδομένα, ακολουθούμενη από δύο στήλες αριθμητικών τιμών. Η πρώτη στήλη περιέχει το μήκος κύματος σε μέτρα (m), ενώ η δεύτερη περιέχει την φασματική ακτινοβολία σε W m−2 m−1. Παρόλο που τα μήκη κύματος αποθηκεύονται σε μέτρα, το πεδίο y_mul της κεφαλίδας επιτρέπει στο OghmaNano να τα εμφανίζει σε νανόμετρα. Το παρακάτω παράδειγμα δείχνει την αρχή του ηλιακού φάσματος 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
...

Ένα σημαντικό πεδίο στην κεφαλίδα είναι το y_len, το οποίο καθορίζει τον αριθμό των σημείων δεδομένων μήκους κύματος-έντασης. Αυτός ο αριθμός πρέπει να ταιριάζει με τον αριθμό των αριθμητικών γραμμών στο αρχείο. Στο παραπάνω παράδειγμα, το y_len είναι 2002, επειδή το πλήρες φάσμα AM1.5G περιέχει 2002 σημεία δεδομένων.

Η περιοχή κάτω από την καμπύλη της φασματικής ακτινοβόλου ισχύος δίνει τη συνολική προσπίπτουσα οπτική ισχύ ανά μονάδα επιφάνειας:

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

Εδώ, το \(I_{\lambda}\) είναι η φασματική ακτινοβολία σε W m−2 m−1, και η προκύπτουσα πυκνότητα ισχύος μετριέται σε W m−2. Για παράδειγμα, το τυπικό ηλιακό φάσμα AM1.5G ενσωματώνεται σε περίπου 1000 W m−2. Είναι σημαντικό τα εισαγόμενα φάσματα να έχουν τη σωστή ενσωματωμένη ένταση, καθώς το OghmaNano χρησιμοποιεί τις τιμές του αρχείου απευθείας και δεν τις κανονικοποιεί αυτόματα.

Οι χρήστες μπορούν επίσης να δημιουργήσουν τα δικά τους φάσματα με μη αυτόματο τρόπο, χωρίς να χρησιμοποιούν το εργαλείο εισαγωγής. Η απλούστερη προσέγγιση είναι να αντιγράψουν έναν υπάρχοντα κατάλογο φάσματος, να αντικαταστήσουν τις τιμές μήκους κύματος και έντασης στο spectra.csv και να ενημερώσουν τις πληροφορίες της κεφαλίδας, ιδίως το y_len. Το αρχείο data.json μπορεί να διατηρηθεί από τον αρχικό κατάλογο. Αυτό παρέχει έναν απλό τρόπο για να ορίσουν προσαρμοσμένα φάσματα λέιζερ, μονοχρωματικές πηγές φωτισμού ή πηγές φωτός που έχουν μετρηθεί πειραματικά.

Windows: ανοίξτε γρήγορα τον τοπικό φάκελο με τα δεδομένα φάσματος.

  1. Τύπος Νίκη+Ρ, τύπος %USERPROFILE%\oghma_local\spectraκαι πατήστε Εισάγετε.
  2. Κάθε φάσμα αποθηκεύεται στον δικό του υποφάκελο (π.χ., ...\spectra\example\).

Γεννήτρια ηλιακού φάσματος

Η εφαρμογή "Δημιουργός Ηλιακού Φάσματος" (??) μπορεί να εκκινήσει από τον "Επεξεργαστή Οπτικού Φάσματος". Συγκρίνει το φάσμα αναφοράς AM1.5G με τις υπολογιζόμενες συνιστώσες Iglobal, Idirect και Idiffuse και επιτρέπει τη δημιουργία φασμάτων για διαφορετικές ώρες της ημέρας, γεωγραφικές τοποθεσίες και ατμοσφαιρικές συνθήκες (πίεση, υγρασία και περιεκτικότητα σε αεροζόλ).

Η γεννήτρια βασίζεται στο απλό ηλιακό φασματικό μοντέλο για την άμεση και διάχυτη ακτινοβολία σε οριζόντιες και κεκλιμένες επιφάνειες στην επιφάνεια της Γης για ατμόσφαιρες χωρίς νέφη, όπως αυτό αναπτύχθηκε από τους Bird και Riordan (1986) και δημοσιεύθηκε στο Journal of Applied Meteorology and Climatology. Μπορείτε να δείτε την αρχική εργασία εδώ. Αυτό το ημι-εμπειρικό μοντέλο υπολογίζει την ηλιακή φασματική ακτινοβολία στην επιφάνεια της Γης, λαμβάνοντας υπόψη βασικές ατμοσφαιρικές διεργασίες, όπως η σκέδαση Rayleigh, η απορρόφηση όζοντος, τα ομοιόμορφα αναμεμιγμένα αέρια, η απορρόφηση υδρατμών και η εξασθένηση των αερολυμάτων. Με δεδομένα εισόδου όπως η τοποθεσία, η ώρα της ημέρας, το υψόμετρο και οι ατμοσφαιρικές παράμετροι, το μοντέλο διαχωρίζει το ηλιακό φάσμα σε συνολικά, άμεσα και διάχυτα συστατικά. Η αναφορά AM1.5G παρέχεται ως σημείο αναφοράς, επιτρέποντας στους χρήστες να δημιουργούν φάσματα ειδικά για την τοποθεσία και τις συνθήκες, τα οποία αντικατοπτρίζουν τόσο τους γεωμετρικούς παράγοντες (γωνία ζενιθ του ήλιου, ατμοσφαιρική μάζα) όσο και τις ατμοσφαιρικές ιδιότητες (πίεση, υδρατμοί και οπτικό βάθος των αερολυμάτων).

OghmaNano Solar Spectrum Generator window showing AM1.5G reference spectrum compared with calculated global, direct, and diffuse components.
Το παράθυρο του Γεννήτορα Ηλιακού Φάσματος. Συγκρίνετε το φάσμα αναφοράς AM1.5G με τις υπολογιζόμενες συνιστώσες (Iglobal, Idirect, Idiffuse) και δημιουργήστε ξανά φάσματα υπό διαφορετικές συνθήκες.

Δημιουργία εξατομικευμένων φασμάτων λέιζερ με τη χρήση της Python.

Εκτός από την εισαγωγή πειραματικά μετρημένων φασμάτων, το OghmaNano επιτρέπει στους χρήστες να δημιουργούν τα δικά τους φάσματα πηγών φωτός χρησιμοποιώντας εξωτερικά σενάρια. Για παράδειγμα, το ακόλουθο πρόγραμμα Python δημιουργεί ρεαλιστικά φάσματα εκπομπής λέιζερ και τα αποθηκεύει απευθείας στη μορφή της οπτικής βάσης δεδομένων του OghmaNano. Αυτό είναι χρήσιμο κατά την προσομοίωση συσκευών που φωτίζονται από λέιζερ, όπως οι φωτοδίοδοι InGaAs που λειτουργούν στα 1550 nm, οι οπτικοί αισθητήρες και οι ημιαγωγικοί φωτοανιχνευτές.

Το σενάριο υποστηρίζει τρία διαφορετικά μοντέλα λέιζερ: ένα φασματικό μοντέλο Gaussian ή Lorentzian για μια απλή πηγή στενής ζώνης, ένα λέιζερ Fabry–Pérot με πολλαπλές διαμήκεις συνιστώσες και ένα λέιζερ ανατροφοδότησης κατανομής (DFB) με μια κυρίαρχη κορυφή εκπομπής και ασθενέστερες πλευρικές συνιστώσες. Μπορεί επίσης να συμπεριληφθεί ένα προαιρετικό υπόβαθρο ενισχυμένης αυθόρμητης εκπομπής (ASE). Το κεντρικό μήκος κύματος, το φασματικό πλάτος και η συνολική οπτική ισχύς μπορούν όλα να ρυθμιστούν από τη γραμμή εντολών.

Για να χρησιμοποιήσετε το σενάριο, αποθηκεύστε τον παρακάτω κώδικα ως make_laser_spectrum.py. Απαιτεί Python 3 και NumPy, ενώ το Matplotlib είναι απαραίτητο μόνο κατά τη δημιουργία ενός προεπισκόπησης γραφήματος. Η εκτέλεσή του χωρίς ορίσματα δημιουργεί έναν φάκελο με όνομα laser_1550nm που περιέχει τα data.json και spectra.csv. Από προεπιλογή, το παραγόμενο φάσμα είναι κεντραρισμένο στα 1550 nm, έχει μια Γκαουσιανή τιμή πλήρους εύρους στο μισό μέγιστο (FWHM) των 2 nm και έχει μια ολοκληρωμένη οπτική πυκνότητα ισχύος 10 W m−2.

python3 make_laser_spectrum.py

Μπορούν να δημιουργηθούν και άλλα μοντέλα λέιζερ αλλάζοντας τις παραμέτρους που δίνονται μέσω της γραμμής εντολών. Για παράδειγμα, η ακόλουθη εντολή δημιουργεί ένα φάσμα λέιζερ Fabry–Pérot με πολλαπλές διαμήκεις συνιστώσες και ένα υπόβαθρο ASE 40 dB χαμηλότερο από την κορυφή:

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

Εναλλακτικά, μια δίοδος λέιζερ DFB με στενή ζώνη εκπομπής μπορεί να παραχθεί χρησιμοποιώντας:

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

Το παραγόμενο φάσμα κανονικοποιείται αυτόματα, έτσι ώστε το ολοκλήρωμα της φασματικής ακτινοβόλου ισχύος του να ισούται με την καθορισμένη οπτική ισχύ. Το σενάριο υπολογίζει επίσης τον αριθμό των σημείων μήκους κύματος και γράφει τη σωστή τιμή y_len στην κεφαλίδα #oghma_csv. Αυτό καθιστά δυνατή τη δημιουργία φασμάτων χωρίς να απαιτείται χειροκίνητη επεξεργασία οποιουδήποτε από τα αρχεία εξόδου.

Αφού δημιουργηθεί, αντιγράψτε τον προκύπτοντα φάκελο στον oghma_local/spectra/. Η νέα πηγή φωτός θα είναι στη συνέχεια διαθέσιμη μέσω της Οπτικής Βάσης Δεδομένων της OghmaNano. Επειδή η έξοδος χρησιμοποιεί την τυπική μορφή φάσματος της OghmaNano, η ίδια διαδικασία μπορεί να χρησιμοποιηθεί για τη δημιουργία μιας βιβλιοθήκης πηγών λέιζερ σε διαφορετικά μήκη κύματος και ισχύς.

🐍 Εμφάνιση του πλήρους κώδικα για τη δημιουργία φασματικού διαγράμματος λέιζερ σε 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()

Δημιουργία φασμάτων εκπομπής LED με τη χρήση της Python.

Εκτός από τα φάσματα λέιζερ και ηλιακής ακτινοβολίας, το OghmaNano υποστηρίζει ανεξάρτητα παραγόμενα φάσματα φωτισμού LED. Το παρακάτω σενάριο Python δημιουργεί 20 αντιπροσωπευτικές πηγές LED ημιαγωγών, καλύπτοντας την υπεριώδη, την ορατή και την υπέρυθρη εκπομπή από 275 nm έως 1550 nm.

Τα παραδείγματα περιλαμβάνουν υπεριώδεις διόδους εκπομπής φωτός AlGaN, μπλε και πράσινες διόδους εκπομπής φωτός InGaN/GaN, κεχριμπαρένιες και κόκκινες διόδους εκπομπής φωτός AlGaInP, καθώς και υπέρυθρες διόδους εκπομπής φωτός GaAs, AlGaAs, InGaAs και InGaAsP. Τα φάσματα παράγονται από φαινομενολογικές εξισώσεις και όχι αντιγράφονται ή προσαρμόζονται από εμπορικές μετρήσεις διόδων εκπομπής φωτός. Τα μήκη κύματος στην κορυφή, τα πλάτη γραμμής και οι οπτικές ισχύς είναι ενδεικτικές παράμετροι και όχι προδιαγραφές συγκεκριμένων προϊόντων.

Φαινομενολογικό μοντέλο εκπομπής

Τα περισσότερα φάσματα χρησιμοποιούν μια απλοποιημένη μορφή της γραμμής φάσματος ενός ημιαγωγού με άμεσο ενεργειακό χάσμα:

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

Εδώ, το \(E\) είναι η ενέργεια του φωτονίου, το \(E_g\) είναι το αποτελεσματικό ενεργειακό χάσμα εκπομπής, το \(k_B\) είναι η σταθερά του Boltzmann και το \(T\) είναι η θερμοκρασία. Εφαρμόζεται διαστολή Gauss πριν από τη μετατροπή σε φάσμα μήκους κύματος. Αυτό παράγει ελαφρώς ασύμμετρα προφίλ εκπομπής αντί για ιδανικά Gauss. Επιλεγμένα παραδείγματα τοπικής εκπομπής, συμπεριλαμβανομένων των GaP:N και GaAs:Si, χρησιμοποιούν μια ξεχωριστή φαινομενολογική μορφή γραμμής.

Οπτική ισχύς και φασματική κανονικοποίηση.

Σε κάθε παράδειγμα αντιστοιχεί μια ενδεικτική εξωτερική οπτική ισχύς. Για μια συνεπή αναφορά φωτισμού, η LED προσεγγίζεται ως μια σημειακή λαμπεριανή πηγή που παρατηρείται στον άξονα σε απόσταση 1 cm. Η προσπίπτουσα ακτινοβολία της υπολογίζεται ως εξής:

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

Αυτή είναι μια κατά προσέγγιση τιμή αναφοράς: η πραγματική ακτινοβολία εξαρτάται από την οπτική διάταξη της μονάδας LED, το μέγεθος του εκπομπού, τον προσανατολισμό και την απόσταση. Η γεννήτρια κανονικοποιεί κάθε φασματική καμπύλη έτσι ώστε:

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

Το OghmaNano αποθηκεύει το μήκος κύματος σε μέτρα και την φασματική ακτινοβολία σε W m−2 m−1. Η ολοκλήρωση της παραγόμενης spectra.csv ως προς το μήκος κύματος δίνει την επιλεγμένη εισερχόμενη ακτινοβολία σε W m−2.

Δημιουργία της βιβλιοθήκης LED.

Αποθηκεύστε το παρακάτω σενάριο ως oghma_led_material_spectra_generator.py. Απαιτεί Python 3 και NumPy. Εκτελέστε το χρησιμοποιώντας:

python3 oghma_led_material_spectra_generator.py

Το σενάριο δημιουργεί τον φάκελο led_material_spectra_20/, ο οποίος περιέχει υποφακέλους για κάθε μήκος κύματος, όπως οι 450nm/, 630nm/ και 870nm/. Κάθε φάκελος μήκους κύματος περιέχει τα τυπικά αρχεία data.json και spectra.csv. Η έξοδος περιλαμβάνει επίσης έναν κατάλογο και ένα αρχείο περιγραφής που περιγράφουν τα παραδείγματα.

Το παράδειγμα στα 870 nm περιγράφει μια διπλή ετεροδομή LED GaAs/Al0.3Ga0.7As. Οι στρώσεις της, από πάνω προς τα κάτω, είναι: 200 nm p-Al0.3Ga0.7As (δοσολογία αποδεκτών 5×1023 m−3), 300 nm ενδογενές GaAs, 200 nm n-Al0.3Ga0.7As (δοσολογία δοτών 5×1023 m−3) και 100 nm n+ GaAs (δοσολογία δοτών 1024 m−3). Το συνολικό πάχος είναι 800 nm. Ο κατάλογος περιλαμβάνει τα device_structure.json και DEVICE_STRUCTURE.txt. αυτά περιγράφουν τη στοίβα των στρώσεων και όχι μια πλήρη ηλεκτρική προσομοίωση.

Για να χρησιμοποιήσετε τις δημιουργημένες πηγές φωτός, αντιγράψτε τους φακέλους με τα μήκη κύματος στον φάκελο oghma_local/spectra/. Στη συνέχεια, μπορείτε να τις επιλέξετε στη Βάση Δεδομένων Οπτικών του OghmaNano και να τις κλιμακώσετε χρησιμοποιώντας την ρύθμιση "Ένταση Φωτός (σε μονάδες Suns)" χωρίς να αλλάξετε το φασματικό τους σχήμα.

🐍 Εμφάνιση του πλήρους κώδικα Python για τη δημιουργία φάσματος LED.
#!/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'))