| name | orca-uv-vis |
| description | Generate ORCA input files for TD-DFT UV-Vis calculations and parse/plot the resulting absorption spectrum. Use when the user asks about UV-Vis spectra, absorption spectra, TD-DFT calculations, excited state calculations in ORCA, or wants to plot results from an ORCA TD-DFT output file. Also trigger when the user mentions oscillator strengths, electronic transitions, or simulated UV-Vis. |
ORCA UV-Vis Calculation Pipeline
This skill covers two stages: generating an ORCA input file for a TD-DFT calculation, and post-processing the output to plot a Gaussian-broadened UV-Vis absorption spectrum.
Scope: Input generation, local post-processing, and (optionally) HPC submission via the CatGo workflow engine. The "Submitting to HPC" section below covers the proven Expanse flow. If the user is running on their own non-CatGo infrastructure, just generate the input file from the template in Stage 1 and skip the submission section.
Target version: ORCA 6.x. Output block layout in ORCA 5 is close but not identical; the parser below is written against the ORCA 6 format used by CatGo's own parser (server/catgo/utils/orca_output.py::OrcaUvVisOutput).
Stage 1: Input Generation
Before generating the input file, ask the user for:
- XYZ geometry file path (assume already optimized; if the user hasn't optimized, route them to
orca-opt first)
- Solvent for CPCM (e.g. hexane, water, ethanol, dichloromethane, acetonitrile)
- Charge and multiplicity (default to
0 1 if not specified)
Generate an ORCA input file with these fixed settings:
- Functional:
CAM-B3LYP
- Basis set:
DEF2-TZVP
- Full TD-DFT (not TDA)
- 30 roots
- CPCM solvation inline on the keyword line
- No RI / auxiliary basis
- No special SCF convergence tricks
Template:
!CAM-B3LYP DEF2-TZVP CPCM(HEXANE)
%TDDFT
NROOTS 30
END
%output jsongbwfile True jsonpropfile True end
*XYZFILE 0 1 geometry.xyz
Substitute the user's solvent, charge, multiplicity, and XYZ path. If the user provides inline coordinates instead of a file, use * XYZ <charge> <mult> followed by the coordinates and close with *.
The %output ... end line makes ORCA emit the JSON files OPI's Output.parse() consumes during post-processing (Stage 2). Keep it.
Building the %tddft block with OPI
OPI's BlockTddft exposes every TD-DFT knob (nroots, iroot, irootmult, maxdim, maxiter, etol, rtol, tda, lrcpcm, cpcmeq, donto, saveunrnatorb, spinflip, soc, socgrad, triplets, ...) as a typed Pydantic field. Bad keys raise at construction.
The catgo backend builds the route line, %pal, %maxcore, charge/multiplicity, and geometry from node params — it does not emit a %TDDFT block of its own. So OPI only contributes the %tddft and %output blocks here, which we paste into extra_blocks as text. Do not use Calculator.write_input() — that writes a full input file and would duplicate the route line / pal / geometry the backend already emits.
from opi.input.blocks import BlockTddft, BlockOutput
tddft_block = BlockTddft(nroots=30, tda=False, triplets=False, donto=True)
output_block = BlockOutput(jsongbwfile=True, jsonpropfile=True)
extra_blocks_text = tddft_block.format_orca() + "\n" + output_block.format_orca()
format_orca() emits exactly one %...end block per call. The result for the snippet above is:
%tddft
nroots 30
tda False
donto True
triplets False
end
%output
jsonpropfile True
jsongbwfile True
end
TD-DFT with 30 roots at def2-TZVP is expensive — tell the user this is a heavy calculation and that they should expect to run it on a cluster or at least overnight on a workstation, not on a laptop.
Submitting to HPC (Expanse) — proven flow
Use this when the user wants the CatGo workflow engine to run the TD-DFT job
on Expanse. Skip if they only want the input file.
Use catgo_workflow (graph-based), NOT catgo_workflow_engine (task-based).
The graph-based tool auto-captures the viewer structure on create. Task-based
add_task doesn't, so jobs fail with "No input structure provided". Param keys
differ: graph-based uses method/basis, task-based uses orca_method/orca_basis.
1. Confirm structure is loaded and find session_id
catgo_view(action: "get_state")
curl -s http://localhost:8000/api/hpc/connections
Copy the session_id for host: login.expanse.sdsc.edu.
2. Create the workflow
catgo_workflow(action: "create", name: "UV-Vis CAM-B3LYP TD-DFT")
3. Add the TD-DFT node
Use the orca_uvvis node type, which has dedicated TD-DFT params (nroots, triplets, tda, donto, solvation, solvent, calc_type, aux_basis) plus the standard dispersion field. Do NOT use extra_keywords or extra_blocks — they are NOT read by the engine and are silently dropped.
catgo_workflow(action: "batch", workflow_id: "<wf_id>", operations: [
{"op": "add_node", "node_type": "orca_uvvis", "label": "tddft",
"params": {
"software": "orca",
"method": "CAM-B3LYP",
"basis": "def2-TZVP",
"calc_type": "tddft",
"nroots": 30,
"tda": false,
"triplets": false,
"donto": true,
)
| Parameter | Default | Description |
|---|
method | CAM-B3LYP | Functional |
basis | def2-TZVP | Basis set |
dispersion | (none) | D4 | D3BJ | D3 |
calc_type | tddft | tddft or steom (STEOM-DLPNO-CCSD) |
nroots | 10 | Number of excited states |
tda | true | Tamm-Dancoff approximation; pass false for full TD-DFT |
triplets | false | Compute triplet states |
donto | false | Natural transition orbitals |
solvation | none | CPCM | none |
solvent | water | Any ORCA-recognised solvent name |
aux_basis | def2-TZVP/C | Aux basis (used by STEOM path) |
num_cores / max_core_mb | 4 / 4000 | |
4. Run with the full HPC run_config
TD-DFT with 30 roots at def2-TZVP is heavy — bump walltime and use shared
or compute (debug caps at 30 min). Read server/templates/orca_generic.sh
and pass its contents as default_template.
catgo_workflow(action: "run", workflow_id: "<wf_id>", run_config: {
"execution_mode": "hpc",
"default_session_id": "<expanse_session_id>",
"base_work_dir": "/expanse/lustre/projects/sdp126/jyang25/ORCA/catgo",
"default_job_params": {
"nodes": 1, "ntasks": 16, "cpus_per_task": 1,
"walltime": "12:00:00", "partition": "shared"
},
"cluster_configs": {
"<expanse_session_id>": {
"account": "sdp126",
"partition":
)
The local-scratch template stages I/O to $TMPDIR/orca_$SLURM_JOB_ID and copies
results back. Required on Expanse — Lustre kills ORCA's many-small-file I/O during
the 30-root TD-DFT response solver.
5. Monitor
catgo_workflow(action: "status", workflow_id: "<wf_id>")
6. Pull files for post-processing
When status is COMPLETED, pull ORCA.out plus the JSON files OPI parses,
then run the parser/plotter from Stage 2 below against the local copy:
mkdir -p ./local_run
for f in ORCA.out ORCA.property.json ORCA.json; do
curl -s -X POST http://localhost:8000/api/hpc/files/read-content \
-H 'Content-Type: application/json' \
-d "{\"session_id\":\"<expanse_session_id>\",\"file_path\":\"<work_dir>/$f\"}" \
> ./local_run/$f
done
Submission gotchas
catgo_workflow_engine.add_task doesn't auto-attach the viewer structure → "No input structure provided".
partition=workq (Shaheen default) is invalid on Expanse → use debug/shared/compute.
partition=debug capped at 30 min — TD-DFT/30-roots almost always needs more.
- Missing
account=sdp126 → "Invalid account or account/partition combination".
- Missing
module_loads + orca_dir → orca not on PATH; the response solver silently produces nothing.
- After re-connecting to Expanse, the session_id changes — re-discover via
/api/hpc/connections and update both default_session_id and the cluster_configs key.
Stage 2: Post-Processing
Reading the Spectrum via OPI
OPI surfaces the absorption spectrum as output.results_properties.geometries[-1].absorption_spectrum — a list[Spectrum] keyed by representation ("Length" / "Velocity") and pointgroup. This replaces the regex parser entirely:
- The
rfind-vs-find STEOM-DLPNO-CCSD workaround is unnecessary; the model returns one entry per (representation, pointgroup) and you pick the one you want.
- The "STEOM rows have state labels, TD-DFT rows don't" branch is unnecessary; both produce the same
Spectrum shape.
- Bonus: ECD rotational strengths come for free at
geometries[-1].ecd_spectrum.
Spectrum.excitationenergies is a list of rows; columns are unnamed in the model but the order for ORCA 6.1.1 is fixed at [energy_eV, energy_cm, wavelength_nm, fosc, |mu|^2]. The shared helper exposes this as UVVIS_COLS.
Reference Parser and Plotter
import sys
sys.path.insert(0, ".claude/skills")
import numpy as np
import matplotlib.pyplot as plt
from _shared.orca_opi import parse_local, UVVIS_COLS
def parse_orca_tddft(work_dir="./local_run"):
out = parse_local(work_dir)
spectra = out.results_properties.geometries[-1].absorption_spectrum
if not spectra:
raise ValueError("No absorption_spectrum block found — check ORCA.property.json")
target = next((s for s in spectra if s.representation == "Length"), spectra[0])
wl_col = UVVIS_COLS["wavelength_nm"]
f_col = UVVIS_COLS["fosc"]
wavelengths = np.array([row[wl_col] for row in target.excitationenergies])
fosc = np.array([row[f_col] for row in target.excitationenergies])
return wavelengths, fosc
def gaussian(x, center, height, sigma=15):
return height * np.exp(-0.5 * ((x - center) / sigma) ** 2)
def plot_uv_vis(wavelengths, fosc, output_png="uv_vis_spectrum.png"):
x = np.linspace(, , )
spectrum = (gaussian(x, w, f) w, f (wavelengths, fosc))
peak = spectrum.()
peak > :
spectrum = spectrum / peak
fig, ax = plt.subplots(figsize=(, ))
ax.plot(x, spectrum, color=, linewidth=, label=)
f_peak = fosc.() fosc.size fosc.() >
ax.vlines(wavelengths, , fosc / f_peak, color=, alpha=, label=)
ax.set_xlabel()
ax.set_ylabel()
ax.set_xlim(, )
ax.set_ylim(, )
ax.legend(loc=, frameon=)
fig.tight_layout()
fig.savefig(output_png, dpi=)
()
__name__ == :
work_dir = sys.argv[] (sys.argv) >
png_name = sys.argv[] (sys.argv) >
wl, f = parse_orca_tddft(work_dir)
plot_uv_vis(wl, f, png_name)
Viewing the spectrum in the IDE
After plot_uv_vis(...) writes the PNG, surface it inline with the shared helper:
from _shared.orca_opi import show_png
show_png("uv_vis_spectrum.png", "UV-Vis spectrum")
Then reply to the user with that markdown link so Claude Code renders the figure inline in chat.
Plotting Defaults
- Wavelength grid: 200–800 nm, 2000 points
- Gaussian broadening: σ = 15 nm in wavelength space (simple and visually reasonable for most organic chromophores; call out that eV-space broadening is more physically correct if the user cares about vibronic lineshapes)
- Envelope normalized to max = 1
- Sticks normalized by the maximum oscillator strength so they sit on the same axis as the envelope
- Figure size 8×5, DPI 150
- Colors: envelope
#2563eb (blue), sticks #ef4444 (red) — matches CatGo's red-TS / blue-accent conventions
Requesting eV on the x-axis
If the user asks for eV instead of nm, convert via E_eV = 1240 / wavelength_nm and rebuild the x-grid from (e.g.) 1.5–6.5 eV. Broadening σ of ~0.3 eV is a sensible default in eV space.