Circular dichroism (CD) with real-time TDDFT

In this tutorial, we calculate the rotatory strength spectrum of (R)-methyloxirane molecule.

The equations underlying the implementation are described in Ref. [1].

LCAO mode

In this example, we use real-time TDDFT LCAO mode. We recall that the CD-LCAO calculations are in general sensitive to the used basis sets, so we will demonstrate how to construct augmented basis sets, and compare them to standard basis sets. Additionally, a choice of gauge is essential. GPAW provides two possibilites, which are selected by providing either gauge='length' (default) or gauge='velocity' keyword argument to the absorption_kick-method.

We augment the default dzp basis sets with numerical Gaussian-type orbitals (NGTOs):

from gpaw.lcao.generate_ngto_augmented import \
    create_GTO_dictionary as GTO, generate_nao_ngto_basis

gtos_atom = {'H': [GTO('s', 0.02974),
                   GTO('p', 0.14100)],
             'C': [GTO('s', 0.04690),
                   GTO('p', 0.04041),
                   GTO('d', 0.15100)],
             'O': [GTO('s', 0.07896),
                   GTO('p', 0.06856),
                   GTO('d', 0.33200)],
             }

for atom in ['H', 'C', 'O']:
    generate_nao_ngto_basis(atom, xc='PBE', name='aug',
                            nao='dzp', gtos=gtos_atom[atom])

Here, the Gaussian parameters correspond to diffuse functions in aug-cc-pvdz basis sets as obtained from Basis Set Exchange.

Let’s do the ground-state calculation (note the basis keyword):

from ase.io import read
from gpaw import setup_paths
from gpaw import GPAW

# Insert the path to the created basis set
setup_paths.insert(0, '.')

atoms = read('r-methyloxirane.xyz')
atoms.center(vacuum=6.0)

calc = GPAW(mode='lcao',
            basis='aug.dzp',
            h=0.3,
            xc='PBE',
            nbands=16,
            poissonsolver={
                'name': 'MomentCorrectionPoissonSolver',
                'poissonsolver': 'fast',
                'moment_corrections': 1 + 3 + 5},
            convergence={'density': 1e-12},
            txt='gs.out',
            symmetry={'point_group': False})
atoms.calc = calc
atoms.get_potential_energy()
calc.write('gs.gpw', mode='all')

Then, we carry the time propagation as usual in real-time TDDFT LCAO mode, but we attach MagneticMomentWriter to record the time-dependent magnetic moment. In this script, we wrap the time propagation code inside main() function to make the same script reusable with gauge='length' or 'velocity' and different kick directions:

from gpaw import setup_paths
from gpaw.lcaotddft import LCAOTDDFT
from gpaw.lcaotddft.dipolemomentwriter import DipoleMomentWriter
from gpaw.lcaotddft.magneticmomentwriter import MagneticMomentWriter

# Insert the path to the created basis set
setup_paths.insert(0, '.')


def main(kick, gauge):
    kick_strength = [0., 0., 0.]
    kick_strength['xyz'.index(kick)] = 1e-5

    td_calc = LCAOTDDFT('gs.gpw', txt=f'td-{kick}.out')

    DipoleMomentWriter(td_calc, f'dm-{gauge}_{kick}.dat')
    MagneticMomentWriter(td_calc, f'mm-{gauge}_{kick}.dat')

    td_calc.absorption_kick(kick_strength, gauge=gauge)
    td_calc.propagate(10, 2000)

    td_calc.write(f'td-{kick}.gpw', mode='all')


if __name__ == '__main__':
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument('--kick', default='x')
    parser.add_argument('--gauge', type=str, default='length')
    kwargs = vars(parser.parse_args())
    main(**kwargs)

Tip

When running the script with gpaw-command, one must add -- before the actual arguments to ensure they are correctly passed the script. For example: $ gpaw -P 4 python td.py -- --gauge=length --kick=x.

After repeating the calculation for kicks in x, y, and z directions in length gauge and velocity gauge, respectively, we calculate the rotatory strength spectrum from the magnetic moments:

from gpaw.tddft.spectrum import rotatory_strength_spectrum

for gauge in ['length', 'velocity']:
    rotatory_strength_spectrum([f'mm-{gauge}_x.dat', f'mm-{gauge}_y.dat',
                               f'mm-{gauge}_z.dat'],
                               f'rot_spec-{gauge}.dat',
                               folding='Gauss', width=0.2,
                               e_min=0.0, e_max=10.0, delta_e=0.01)

Comparing the resulting spectrum to one calculated with plain dzp basis sets shows the importance of augmented basis sets. For comparison of different gauge choices, see below at the next section Origin dependence.

../../../_images/spectra.png

Origin dependence

Length gauge

Circular dichroism spectra obtained in length gauge can exhibit pronounced origin dependence. Below, this is demonstrated by plotting the circular dichroism spectra calculated in the length gauge using different origins. See the documentation of MagneticMomentWriter for parameters controlling the origin location.

The magnetic moment data can be written at multiple different origins during a single propagation as demonstrated in this script:

from gpaw import setup_paths
from gpaw.lcaotddft import LCAOTDDFT
from gpaw.lcaotddft.dipolemomentwriter import DipoleMomentWriter
from gpaw.lcaotddft.magneticmomentwriter import MagneticMomentWriter

# Insert the path to the created basis set
setup_paths.insert(0, '.')


def main(kick, gauge):
    kick_strength = [0., 0., 0.]
    kick_strength['xyz'.index(kick)] = 1e-5

    td_calc = LCAOTDDFT('gs.gpw', txt=f'td-{kick}.out')

    DipoleMomentWriter(td_calc, f'dm-{gauge}_{kick}.dat')

    # Origin: center of mass
    MagneticMomentWriter(td_calc, f'mm-COM-{gauge}_{kick}.dat',
                         origin='COM')

    # Origin: center of mass + 5 Å shift
    for shift_axis in 'xyz':
        origin_shift = [0, 0, 0]
        origin_shift['xyz'.index(shift_axis)] = 5
        MagneticMomentWriter(td_calc,
                             f'mm-COM+{shift_axis}-{gauge}_{kick}.dat',
                             origin='COM', origin_shift=origin_shift)

    # Origin: arbitrary coordinate
    MagneticMomentWriter(td_calc, f'mm-123-{gauge}_{kick}.dat',
                         origin='zero', origin_shift=[1, 2, 3])

    td_calc.absorption_kick(kick_strength, gauge=gauge)
    td_calc.propagate(10, 2000)

    td_calc.write(f'td-{kick}.gpw', mode='all')


if __name__ == '__main__':
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument('--kick', default='x')
    parser.add_argument('--gauge', default='length')
    kwargs = vars(parser.parse_args())
    main(**kwargs)

Velocity gauge

In the velocity gauge, circular dichroism spectra are less origin dependent since the velocity form of the dipole operator satisfies gauge invariance more naturally in comparison to its length gauge form. By reducing of the artificial dependence on the choice of coordinate origin during dipole moment calculations, the velocity gauge can provide more consistent and physically meaningful circular dichroism spectra across different origins.

In the following image, the length and velocity gauge spectra with different origins are compared. We see that the velocity gauge has much smaller origin dependence:

../../../_images/spectra_origins.png

FD mode

In this example, we use real-time TDDFT FD mode.

Let’s do the ground-state calculation (note that FD mode typically requires larger vacuum and smaller grid spacing than LCAO mode; convergence with respect to these parameters need to be checked for both modes separately):

from ase.io import read
from gpaw import GPAW

atoms = read('r-methyloxirane.xyz')
atoms.center(vacuum=8.0)

calc = GPAW(mode='fd',
            h=0.2,
            xc='PBE',
            nbands=16,
            poissonsolver={
                'name': 'MomentCorrectionPoissonSolver',
                'poissonsolver': 'fast',
                'moment_corrections': 1 + 3 + 5},
            convergence={'density': 1e-12},
            txt='gs.out',
            symmetry={'point_group': False})
atoms.calc = calc
atoms.get_potential_energy()
calc.write('gs.gpw', mode='all')

Then, similarly to LCAO mode, we carry the time propagation as usual and attach MagneticMomentWriter to record the time-dependent magnetic moment (note the tolerance parameter for the iterative solver; smaller values might be required when signal is weak):

from gpaw.tddft import TDDFT, DipoleMomentWriter, MagneticMomentWriter


def main(kick):
    kick_strength = [0., 0., 0.]
    kick_strength['xyz'.index(kick)] = 1e-5

    td_calc = TDDFT('gs.gpw',
                    solver=dict(name='CSCG', tolerance=1e-8),
                    txt=f'td-{kick}.out')

    DipoleMomentWriter(td_calc, f'dm-{kick}.dat')
    MagneticMomentWriter(td_calc, f'mm-{kick}.dat')

    td_calc.absorption_kick(kick_strength)
    td_calc.propagate(10, 2000)

    td_calc.write(f'td-{kick}.gpw', mode='all')


if __name__ == '__main__':
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument('--kick', default='x')
    kwargs = vars(parser.parse_args())
    main(**kwargs)

After repeating the calculation for kicks in x, y, and z directions, we calculate the rotatory strength spectrum from the magnetic moments:

from gpaw.tddft.spectrum import rotatory_strength_spectrum

rotatory_strength_spectrum(['mm-x.dat', 'mm-y.dat',
                           'mm-z.dat'],
                           'rot_spec.dat',
                           folding='Gauss', width=0.2,
                           e_min=0.0, e_max=10.0, delta_e=0.01)

The resulting spectrum:

../../../_images/spectrum.png

The spectrum compares well with the one obtained with LCAO mode and augmented basis sets.

References