If you have ever run a TD-DFT calculation in Gaussian and stared at the output wondering exactly what each excited state is made of — which orbitals, on which atoms, with what percentage — this post is for you.
Gaussian gives you the excitation energies, the wavelengths, the oscillator strengths, and the list of one-electron transitions with their coefficients. What it does not give you directly is the chemically meaningful picture: is this band a Metal-to-Ligand Charge Transfer? An intraligand transition? How much does HOMO → LUMO contribute, and how much of that HOMO actually lives on the metal? Answering those questions is normally a matter of going back and forth between the orbital population section and the excited state section of the log file, doing mental arithmetic, and hoping you don’t mix up a sign. It’s tedious, and when you have 40 excited states and a dozen compounds to compare, it becomes a real bottleneck.
ELOTE (Electron Localization of Transition Excitations) is a Python code I wrote to automate precisely that workflow. You give it a Gaussian 16 log file, it gives you a complete, publication-ready assignment table — with HOMO/LUMO labels, percentage contributions, MLCT/LMCT/IL character, and two output files you can paste directly into a manuscript or load into pandas for further analysis.
Let’s go through it step by step.
What you need from Gaussian
ELOTE reads a standard Gaussian 16 log file (.log or .out). The route section needs two things:
- The TD keyword (obviously), with however many states you need. For metal complexes I usually go with
TD=(Nstates=40)as a starting point; for high-energy regions bump it up to 80. - The pop=allorbitals keyword — or at minimum
pop=orbitals— so that Gaussian prints the atomic orbital composition of each molecular orbital. Without this, ELOTE can still process the excited states, but it won’t be able to assign HOMO/LUMO labels or MLCT/LMCT character.
For best results, lower the printing threshold as well:
#P B3LYP/def2-SVP TD=(Nstates=40) pop=(allorbitals,ThreshOrbitals=1)
The default threshold in Gaussian is 10%, meaning contributions below that are simply not printed. Using ThreshOrbitals=1 drops it to 1%, which matters a lot for minor but chemically significant contributions. Note that this will make your log file considerably larger.
That’s the only input change you need. Run your calculation normally and keep the log file.
Installing ELOTE
ELOTE has minimal dependencies — just Python 3 and pandas. If you already use Python for anything computational, you almost certainly have both.
pip install pandas
Then grab the script from the GitHub repository:
https://github.com/[YOUR_USERNAME]/ELOTE
And you’re ready.
Running it
The basic call is as simple as it gets:
python3 ELOTE.py my_calculation.log
There are two optional flags worth knowing about:
python3 ELOTE.py my_calculation.log --sort-f
This sorts the output table by descending oscillator strength, which is very handy when you want to focus on the most intense absorption bands first.
python3 ELOTE.py my_calculation.log --min-contrib 5.0
This suppresses any transition contributing less than 5% from the terminal output. It keeps things clean when you have excited states with many minor contributions that clutter the table.
Walking through the output
Let me use a copper(I) iodide complex as a working example — the kind of system where MLCT assignments are central to understanding the photophysics.
The relevant part of the Gaussian log looks like this (slide for full view):
Alpha occ 148 OE=-0.229 is Cu41-d=0.6239 Cu41-p=0.3778
Alpha occ 149 OE=-0.207 is Cu41-d=0.5105 Cu41-p=0.4153
Alpha occ 150 OE=-0.184 is Cu41-p=0.8112 Cu41-s=0.3890 I51-s=-0.1978
Alpha vir 151 OE=-0.040 is C6-p=0.1123
Alpha vir 152 OE=-0.037 is N42-p=0.1403
Alpha vir 153 OE=-0.022 is C1-p=0.1253 C4-p=0.1071 N42-p=0.1069 C22-p=0.1034
Section 1: Molecular Orbital Composition
The first thing ELOTE prints is the full orbital composition table. It identifies orbital 150 as HOMO (last occupied) and 151 as LUMO (first virtual), and labels everything accordingly. The raw coefficients from Gaussian are multiplied by 100 so you read them directly as percentages (slide for full view):
======================================================================Molecular Orbital percentage composition in Atomic Orbitals======================================================================Default threshold is 10%. To decrease threshold use ThreshOrbitals=n(n < 10) in the route section as an option for the pop keywordHOMO-2 (148) OE=-0.229 percentage is Cu41-d=62.39 Cu41-p=37.78HOMO-1 (149) OE=-0.207 percentage is Cu41-d=51.05 Cu41-p=41.53HOMO (150) OE=-0.184 percentage is Cu41-p=81.12 Cu41-s=38.90 I51-s=-19.78LUMO (151) OE=-0.040 percentage is C6-p=11.23LUMO+1 (152) OE=-0.037 percentage is N42-p=14.03LUMO+2 (153) OE=-0.022 percentage is C1-p=12.53 C4-p=10.71 N42-p=10.69 C22-p=10.34
Already useful! You can see at a glance that the HOMO is predominantly copper-centered (81% p character + 39% s), while all three virtual orbitals are ligand-centered (carbon and nitrogen p orbitals). This tells you before even looking at the excited states that transitions from HOMO outward will be MLCT in character.
Section 2: Excited State Contributions
Now for the main event. The first excited state in the Gaussian output reads (slide for full view):
Excited State 1: Singlet-A 2.1376 eV 580.02 nm f=0.0198 <S**2>=0.000
150 ->151 0.51251
150 ->152 -0.30648
150 ->153 0.33919
ELOTE computes the normalization constant B — the sum of all squared coefficients — and uses it to convert each contribution to a percentage. The sign of the coefficient doesn’t matter here, since what we care about is the squared value. The output is (slide for full view):
Excited State 1: Singlet-A 2.1376 eV 580.02 nm f=0.0198 <S**2>=0.000 150 -> 151 HOMO -> LUMO 55.69 % [¹MLCT] 150 -> 152 HOMO -> LUMO+1 19.92 % [¹MLCT] 150 -> 153 HOMO -> LUMO+2 24.39 % [¹MLCT] ────────────────────────────────────────────────── TOTAL Contribution: 100.00 %
A few things to notice here. First, the HOMO/LUMO labels replace the raw orbital numbers, so the assignment is immediately readable. Second, ELOTE automatically detects that copper is a transition metal and classifies each transition: since the HOMO is >50% copper and the LUMOs are ligand-centered, all three transitions are ¹MLCT (singlet Metal-to-Ligand Charge Transfer). The superscript 1 comes from the symmetry label — “Singlet-A” → ¹. Third, the TOTAL line is printed explicitly. If it reads something like 87.35%, it means Gaussian didn’t print all the contributing transitions (usually because of the 10% threshold), and you should consider rerunning with ThreshOrbitals=1.
Section 3: Summary Table
After processing all excited states, ELOTE prints the summary table you’ll actually copy into your paper (slide for full view):
======================================================================SUMMARY TABLE====================================================================== λcalcd (nm) f Transition (% contribution) Assignment E (eV)───────────────────────────────────────────────────────────────────────────────────── 580.02 0.0198 HOMO→LUMO (56) HOMO→LUMO+2 (24) HOMO→LUMO+1 (20) ¹MLCT ¹MLCT ¹MLCT 2.1376 381.48 0.1542 HOMO-1→LUMO (75) HOMO-2→LUMO+1 (25) ¹MLCT ¹MLCT 3.2500
This is the table format you see in virtually every inorganic photochemistry paper. The Assignment column only appears if ELOTE detects transition metals in the molecule — for purely organic systems it is omitted automatically.
Output files
ELOTE generates two files, both named after your log file:
my_calculation_ELOTE.csv — the summary table as a CSV, with columns for λ (nm), f, the transition string, assignment (if applicable), E (eV), symmetry, and S². This loads directly into Excel or pandas.
my_calculation_ELOTE_output.txt — the complete terminal output captured to a text file. Everything you see on screen is also written here, from the opening banner to the closing one. This is useful for archiving your analysis alongside the Gaussian log file, and also comes in handy when reviewers ask for the raw assignment data.
A note on the math
For those who like to know what’s happening under the hood: Gaussian expresses each excited state as a linear combination of one-electron transitions (from occupied MO i to virtual MO j), each with a coefficient Cij. ELOTE computes a normalization constant:
B = Σ C²ᵢⱼ
and the percentage contribution of each transition is then:
contrib (%) = (C²ᵢⱼ / B) × 100
The sum over all transitions for a given excited state should be 100% (or very close to it, within floating-point rounding). If it’s significantly below 100%, it means the printed transitions don’t account for the full excitation — increase your Nstates value or lower the ThreshOrbitals threshold. ELOTE prints the running total precisely so you can catch this immediately.
Tips for metal complexes
A few practical things I’ve learned that make ELOTE output more useful:
- Use
pop=(allorbitals,ThreshOrbitals=1)always for metal complexes. Transition metals often have many d-type orbitals with small but chemically important contributions that fall below the default 10% cutoff. - For complexes with many unpaired electrons (doublets, triplets), check the S² values in the output. Values significantly above the expected value (0.0 for singlets, 0.75 for doublets, 2.0 for triplets) indicate spin contamination, and the excitation assignments should be treated with care.
- High-energy excitations (below 300 nm for most systems) often need more states —
TD=(Nstates=80)is a reasonable upper bound before the calculation becomes very expensive. - If ELOTE reports metals detected but the MLCT/LMCT assignments look chemically wrong, check whether the metal’s orbital composition is being printed with enough contributions. The 10% threshold can sometimes hide a dominant d-orbital contribution.
Putting it together
The complete workflow for a transition metal complex is:
- Run Gaussian with
TD=(Nstates=40) pop=(allorbitals,ThreshOrbitals=1) - Run
python3 ELOTE.py your_file.log - Check the MO composition table — make sure your frontier orbitals make chemical sense
- Scan the excited state contributions — verify TOTAL values are close to 100%
- Use the summary table (or the CSV) for your manuscript
That’s it. What used to take an afternoon of log file archaeology now takes about ten seconds.
ELOTE is open source and freely available at https://github.com/joaquinbarroso/ELOTE. If you find bugs, have suggestions, or want to contribute, feel free to open an issue or a pull request. Tutorials will live at https://joaquinbarroso.com/category/ELOTE as the code evolves.
I hope you find it useful — as always, questions and comments are welcome below!