Absorption and emission spectra

This tutorial shows how to calculate and plot UV/vis spectra via single-point convolution and nuclear-ensemble approach (NEA). ML can be used to increase precision of the NEA spectra at reduced cost.

Aborption spectra

Single-point convolution

Here are the required files:

uvvis_spc

Nuclear-ensemble approach

Here are the required files: - Jupyter notebook: uvvis_nea.ipynb, - experimental UV/vis spectrum of benzene from the NIST database: benzene_exp.dat, - pre-calculated excited-state properties for an ensemble: benzene_ensemble.json

uvvis_nea

ML-NEA

NEA is pretty expensive and, hence, we can use ML to speed up the calculations, e.g., with our ML-NEA approach:

ML-NEA workflow

[Credit: Pavlo O. Dral, Fuchun Ge, Bao-Xin Xue, Yi-Fan Hou, Max Pinheiro Jr, Jianxing Huang, Mario Barbatti. MLatom 2: An Integrative Platform for Atomistic Machine Learning. Top. Curr. Chem. 2021, 379, 27. DOI: 10.1007/s41061-021-00339-5, under CC-BY license.]

The following example is based on the precalculated data from our book chapter.

MLatom input file:

cross-section
Nexcitations=30
plotQCNEA
plotQCSPC
deltaQCNEA=0.05

These calculations require many data files (reference excitation energies at TDDFT level). These data files are zipped. You can upload them as an auxiliary file to the XACS cloud.

Calculations can take more than 5 min. MLatom automatically determines the minimum required number of training points, in this case it needed 200 points for precise spectrum. In the output file you can find that it took 4 iterations to converge:

==========================================================================================
run ML-NEA iteratively for spectrum generation ( ML_train_iter ) started at Wed Dec  1 12:00:19 2021 CST
ML-NEA iteration 1: train_number = 50; RMSE_geom = 0.06717941145022376; rRMSE = 1.0

ML-NEA iteration 2: train_number = 100; RMSE_geom = 0.09043318436728051; rRMSE = 0.25713761026721255

ML-NEA iteration 3: train_number = 150; RMSE_geom = 0.06411060145373663; rRMSE = 0.410580813729204

ML-NEA iteration 4: train_number = 200; RMSE_geom = 0.0695737045717655; rRMSE = 0.07852252732055763

ML-NEA iteration ended after 4 iteration!
run ML-NEA iteratively for spectrum generation ( ML_train_iter ) finished at Wed Dec  1 12:08:01 2021 CST |||| total spent 462.02 sec
==========================================================================================

After the calculations finished, the spectra are plotted to plot.png file in the cross-section sub-directory. It should look like:

_images/ml-nea-plot.png

The final result: “ref” is the experimental spectrum, QC-NEA – spectrum calculated with quantum chemical approach on 200 points in ensemble, ML-NEA – machine learning spectrum generated with 200 points in the training set and 50k points in ensemble, QC-SPC – spectrum generated with single-point convolution.

You generate such a spectrum from scratch as described in the manual.

See also a more detailed tutorial.

Emission spectra

Emission spectra simulations are similar to the absorption spectra, with some exceptions. The common technique is optimizing the molecule’s geometry in the first excited state and checking whether the oscillator strengths are high. You can use the single-point convolution, e.g., broaden the peaks with the Gaussian broadening. Geometry optimization requires passing the arguments to the model during the geometry optimizations. These arguments define the number of electronic states to be calculated (including the ground state) and the current state (starts with zero for the ground state).

A code snippet:

# we need to optimize the S1 excited state
# .. request the calculation for the S1 state
#    using the current_state argument passed to the method while making predictions
model_kwargs = {'nstates': 2, 'current_state': 1,
                'calculate_energy_gradients': [True]*2 # required by some methods such as AIQM1 or OM2
                }
ml.optimize_geometry(molecule=mol, model=mymodel, model_predict_kwargs=model_kwargs)
print('Optimized geometry:\n')
print(mol.get_xyz_string())

# show the excitation energies and oscillator strengths
print(f'Excitation energies in eV: {mol.excitation_energies*ml.constants.hartree2eV}')
print(f'Oscillator strengths: {mol.oscillator_strengths}')