UV–Visible Spectra with Tako Script
On this page
tako.stda calculates discrete sTDA states and a Gaussian-folded energy-domain spectrum with either GFN2-xTB or g-xTB. GFN2 is the accuracy-first default and invokes the established standalone xtb4stda ground-state/response path. Selecting g-xTB instead runs its normal SCF and passes that accepted wavefunction directly into the sTDA response. Theory belongs in Electronic Excitations and UV–Visible Spectra; visible controls belong in Simulate a UV–Visible Spectrum.
Run a calculation
const molecule = await tako.input("input/chromophore.xyz");
const calculator = tako.calculator.gfn2({
charge: 0,
unpaired: 0,
});
const result = await tako.stda(molecule, {
calculator,
properties: ["energy", "forces"],
nstates: 32,
includeTransitionVectors: true,
spectrumEminEv: 1.5,
spectrumEmaxEv: 6.5,
spectrumSpacingEv: 0.01,
spectrumSigmaEv: 0.15,
});
await tako.output("output/stda-result.json", result);
Charge and unpaired-electron count must describe a valid integral occupation. Geometry is used as supplied. The call performs no optimization, conformer search, solvent response, excited-state relaxation, or vibronic calculation.
To use the faster route explicitly, construct tako.calculator.gxtb() instead. The result then reports level_of_theory: "gxtb" and method: "stdaGxtb". This route imports the accepted orbitals, energies, and occupations, preserves the cached density unchanged, and evaluates response dipole integrals in the calculator-owned q-vSZP basis. Tako never switches between the two routes after dispatch and does not choose a method from the molecule or residual.
properties is accepted because sTDA passes through the shared calculation-options parser. It is normalized like other calculation property lists, but the dedicated response operation does not use it to select output channels. Requesting charges, dos, stress, or another common property does not add that field to the sTDA result; run the appropriate separate operation when those data are required.
Separate electronic and plotting controls
nstates changes the response eigenproblem and requests exactly that many lowest positive roots. spectrumEminEv, spectrumEmaxEv, spectrumSpacingEv, and spectrumSigmaEv only construct the curve from those completed roots. Widening the curve cannot recover states that were never requested.
Use the highest returned energy to test coverage:
const highest = result.states.at(-1)?.energy_ev;
if (highest == null || highest < 6.5 + 3 * result.settings.spectrum_sigma_ev) {
throw new Error("Increase nstates before interpreting the upper spectrum window");
}
The runtime caps nstates at 256. It requires the selected transition space to supply the exact requested count; otherwise it rejects the operation.
Understand normalization
Defaults are 16 states, transition coefficients enabled, 0–10 eV, 0.02 eV spacing, and 0.15 eV Sigma. State count is clamped to 1–256. The minimum is clamped to zero. An invalid or equal interval is repaired to a positive width. Nonpositive spacing and Sigma fall back to defaults; effective spacing is at least 0.0001 eV.
The curve point count is
const expectedPoints = Math.floor(
(result.settings.spectrum_emax_ev - result.settings.spectrum_emin_ev) /
result.settings.spectrum_spacing_ev
) + 1;
There is no grid-size cap. Validate generated calls before dispatch; a tiny spacing over a huge finite interval can exhaust memory.
Read discrete states
Each state contains:
for (const state of result.states) {
const {
index, // zero-based
energy_ev,
oscillator_strength, // dimensionless, nonnegative
transition_dipole_au, // [x, y, z]
dominant_transitions, // [] when disabled
} = state;
}
transition_dipole_au is an electronic transition dipole, not a permanent dipole. The oscillator strength follows in atomic units.
When includeTransitionVectors is true, at most four dominant components are returned:
const components = state.dominant_transitions
.slice()
.sort((a, b) => Math.abs(b.weight) - Math.abs(a.weight));
for (const component of components) {
// spin is null, "alpha", or "beta"
// occupied and virtual_orbital are zero-based model MO indices
// weight is a signed eigenvector coefficient, not a percentage
}
Do not square and renormalize the four values as though they were a complete eigenvector; omitted components remain. Do not infer state multiplicity from spin, which identifies an unrestricted orbital channel.
Clearing includeTransitionVectors changes only the compact coefficient list. Energies, dipoles, oscillator strengths, and the curve are unchanged.
Read the Gaussian curve
result.uv_vis.energies_ev and result.uv_vis.intensities are parallel arrays. The intensity is
It is effectively oscillator strength per eV, not molar absorptivity or absorbance. On an infinite interval each line integrates to ; a finite result truncates tails.
Check the actual endpoint instead of assuming E max is present:
const energies = result.uv_vis.energies_ev;
const intensities = result.uv_vis.intensities;
if (energies.length !== intensities.length || energies.length === 0) {
throw new Error("Malformed UV/Vis curve arrays");
}
const actualMax = energies.at(-1);
Sigma is a standard deviation. Convert it to FWHM with 2.35482 * sigma; do not label Sigma as FWHM.
Preserve the result boundary
The top-level schema is:
{
kind: "stda",
level_of_theory: "gfn2", // or "gxtb"
engine: "gfn2",
method: "stdaXtb", // "stdaGxtb" for g-xTB
states: [...],
excitations: { method: "stdaXtb", states: [...] }, // same method identity
uv_vis: {
kind: "uv_vis",
x_unit: "eV",
energies_ev: [...],
intensities: [...],
sigma_ev: 0.15,
},
settings: {...},
}
states and excitations.states are duplicates, not independent solutions. The lower API’s structure is dropped at the WASM boundary. The result contains no charge/spin echo, orbital energies, HOMO index, parameter version, state symmetry/multiplicity, convergence diagnostic, excited-state gradient, transition density, or uncertainty. Preserve inputs alongside it.
The interactive workspace additionally extracts excitations.json and uv_vis.json; those artifacts are not extra fields returned by a plain Script output call. Viewer peaks, wavelengths, frontier labels, brightness classes, assignments, and contribution chips are frontend derivations and do not belong to this schema.
Find peaks reproducibly
For quantitative work, define and save the peak algorithm rather than copying viewer labels. A minimal sampled-grid detector is:
const peaks = [];
for (let i = 1; i < intensities.length - 1; i += 1) {
if (intensities[i] >= intensities[i - 1] && intensities[i] > intensities[i + 1]) {
peaks.push({ energyEv: energies[i], intensity: intensities[i] });
}
}
peaks.sort((a, b) => b.intensity - a.intensity);
This intentionally excludes boundary maxima and does not assign states. For a state contribution at peak energy , use each Gaussian value at , not normalized oscillator strengths alone.
Compare calculations
Keep interval, spacing, Sigma, charge/spin policy, geometry protocol, and root-coverage criterion fixed. Compare discrete roots before folded curves. Store raw unshifted energies and document any later empirical shift or normalization.
The standalone GFN2 route ignores calculator solvent, D4, SCF, and mixer settings because it owns a dedicated xtb4stda ground-state solve. The g-xTB route reuses its accepted SCF state, so its SCF controls affect convergence and the resulting orbitals; implicit solvation remains unsupported. Neither route applies a residual-derived excitation correction. If environment or conformer effects matter, generate explicit structure/protocol variants and label them.
Handle failure and cancellation
Invalid charge/spin, missing element parameters, invalid periodic lattice, insufficient selected transitions or positive roots, eigensolver failure, and memory pressure reject the call. A successful result contains no accuracy flag. Cancellation discards the worker calculation; there is no resumable transition-space or eigenpair checkpoint.
The exhaustive field types, defaults, normalization, and result members are generated in tako.stda Settings.