# Hybrid Quantum/Classical Scheme¶

The basic idea is to separate the calculation into two parts: the first one is the quantum subsystem, which is propagated using Time-propagation TDDFT scheme, and the second one is the classical subsystem that is treated using Quasistatic Finite-Difference Time-Domain method. The subsystems are propagated separately in their own real space grids, but they share a common electrostatic potential.

In the Time-propagation TDDFT part of the calculation the electrostatic potential is known as the Hartree potential $$V^{\rm{qm}}(\mathbf{r}, t)$$ and it is solved from the Poisson equation $$\nabla^2 V^{\rm{qm}}(\mathbf{r}, t) = -4\pi\rho^{\rm{qm}}(\mathbf{r}, t)$$

In the Quasistatic Finite-Difference Time-Domain method the electrostatic potential is solved from the Poisson equation as well: $$V^{\rm{cl}}(\mathbf{r}, t) = -4\pi\rho^{\rm{cl}}(\mathbf{r}, t).$$

The hybrid scheme is created by replacing in both schemes the electrostatic (Hartree) potential by a common potential: $$\nabla^2 V^{\rm{tot}}(\mathbf{r}, t) = -4\pi\left[\rho^{\rm{cl}}(\mathbf{r}, t)+\rho^{\rm{qm}}(\mathbf{r}, t)\right].$$

## Double grid¶

The observables of the quantum and classical subsystems are defined in their own grids, which are overlapping but can have different spacings. The following restrictions must hold:

• The quantum grid must fit completely inside the classical grid

• The spacing of the classical grid $$h_{\rm{cl}}$$ must be equal to $$2^n h_{\rm{qm}}$$, where $$h_{\rm{qm}}$$ is the spacing of the quantum grid and n is an integer.

When these conditions hold, the potential from one subsystem can be transferred to the other one. The grids are automatically adjusted so that some grid points are common.

## Transferring the potential between two grids¶

• Transferring the potential from classical subsystem to the quantum grid is performed by interpolating the classical potential to the denser grid of the quantum subsystem. The interpolation only takes place in the small subgrid around the quantum mechanical region.

• Transferring the potential from quantum subsystem to the classical one is done in another way: instead of the potential itself, it is the quantum mechanical electron density $$\rho^{\rm{qm}}(\mathbf{r}, t)$$ that is copied to the coarser classical grid. Its contribution to the total electrostatic potential is then determined by solving the Poisson equation in that grid.

• Altogether this means that although there is only one potential to be determined $$(V^{\rm{tot}}(\mathbf{r}, t))$$, three Poisson equations must be solved:

1. $$V^{\rm{cl}}(\mathbf{r}, t)$$ in classical grid

2. $$V^{\rm{qm}}(\mathbf{r}, t)$$ in quantum grid

3. $$V^{\rm{qm}}(\mathbf{r}, t)$$ in classical grid

When these are ready and $$V^{\rm{cl}}(\mathbf{r}, t)$$ is transferred to the quantum grid, $$V^{\rm{tot}}(\mathbf{r}, t)$$ is determined in both grids.

## Example: photoabsorption of Na2 near gold nanosphere¶

This example calculates the photoabsorption of $$\text{Na}_2$$ molecule in (i) presence and (ii) absence of a gold nanosphere:

from ase import Atoms
from gpaw.fdtd.poisson_fdtd import QSFDTD
from gpaw.fdtd.polarizable_material import (PermittivityPlus,
PolarizableMaterial,
PolarizableSphere)
from gpaw.tddft import photoabsorption_spectrum
import numpy as np

# Geometry
atom_center = np.array([30., 15., 15.])
sphere_center = np.array([15., 15., 15.])
simulation_cell = np.array([40., 30., 30.])

# Atoms object
atoms = Atoms('Na2', atom_center + np.array([[-1.5, 0.0, 0.0],
[1.5, 0.0, 0.0]]))

# Permittivity of Gold
# J. Chem. Phys. 135, 084121 (2011); http://dx.doi.org/10.1063/1.3626549
eps_gold = PermittivityPlus(data=[[0.2350, 0.1551, 95.62],
[0.4411, 0.1480, -12.55],
[0.7603, 1.946, -40.89],
[1.161, 1.396, 17.22],
[2.946, 1.183, 15.76],
[4.161, 1.964, 36.63],
[5.747, 1.958, 22.55],
[7.912, 1.361, 81.04]])

# 1) Nanosphere only
classical_material = PolarizableMaterial()
permittivity=eps_gold))

qsfdtd = QSFDTD(classical_material=classical_material,
atoms=None,
cells=simulation_cell,
spacings=[2.0, 0.5],
remove_moments=(1, 1))

energy = qsfdtd.ground_state('gs.gpw',
nbands=1)

qsfdtd.time_propagation('gs.gpw',
kick_strength=[0.001, 0.000, 0.000],
time_step=10,
iterations=1500,
dipole_moment_file='dm.dat')

photoabsorption_spectrum('dm.dat', 'spec.1.dat', width=0.15)

classical_material = PolarizableMaterial()
permittivity=eps_gold))

qsfdtd = QSFDTD(classical_material=classical_material,
atoms=atoms,
cells=(simulation_cell, 4.0),  # vacuum = 4.0 Ang
spacings=[2.0, 0.5],
remove_moments=(1, 1))

energy = qsfdtd.ground_state('gs.gpw', nbands=-1)

qsfdtd.time_propagation('gs.gpw',
kick_strength=[0.001, 0.000, 0.000],
time_step=10,
iterations=1500,
dipole_moment_file='dm.dat')

photoabsorption_spectrum('dm.dat', 'spec.2.dat', width=0.15)

# 3) Nanosphere + Na2
classical_material = PolarizableMaterial()
permittivity=eps_gold))

qsfdtd = QSFDTD(classical_material=classical_material,
atoms=atoms,
cells=(simulation_cell, 4.0),  # vacuum = 4.0 Ang
spacings=[2.0, 0.5],
remove_moments=(1, 1))

energy = qsfdtd.ground_state('gs.gpw', nbands=-1)

qsfdtd.time_propagation('gs.gpw',
kick_strength=[0.001, 0.000, 0.000],
time_step=10,
iterations=1500,
dipole_moment_file='dm.dat')

photoabsorption_spectrum('dm.dat', 'spec.3.dat', width=0.15) The optical response of the molecule apparently enhances when it is located near the metallic nanoparticle, see Ref.  for more examples. The geometry and the distribution of the grid points are shown in the following figure (generated with this script): ## Advanced example: Near field enhancement of hybrid system¶

In this example we calculate the same hybrid Na2 + gold nanoparticle system as above, but using the advanced syntax instead of the QSFDTD wrapper. This allows us to include InducedField observers in the calculation, see TDDFTInducedField module documentation:

from ase import Atoms
from gpaw import GPAW
from gpaw.fdtd.poisson_fdtd import FDTDPoissonSolver
from gpaw.fdtd.polarizable_material import (PermittivityPlus,
PolarizableMaterial,
PolarizableSphere)
from gpaw.tddft import TDDFT, DipoleMomentWriter, photoabsorption_spectrum
from gpaw.inducedfield.inducedfield_tddft import TDDFTInducedField
from gpaw.inducedfield.inducedfield_fdtd import FDTDInducedField
from gpaw.mpi import world
import numpy as np

# Geometry
atom_center = np.array([30., 15., 15.])
sphere_center = np.array([15., 15., 15.])
simulation_cell = np.array([40., 30., 30.])

# Atoms object
atoms = Atoms('Na2', atom_center + np.array([[-1.5, 0.0, 0.0],
[1.5, 0.0, 0.0]]))

# Permittivity of Gold
# J. Chem. Phys. 135, 084121 (2011); http://dx.doi.org/10.1063/1.3626549
eps_gold = PermittivityPlus(data=[[0.2350, 0.1551, 95.62],
[0.4411, 0.1480, -12.55],
[0.7603, 1.946, -40.89],
[1.161, 1.396, 17.22],
[2.946, 1.183, 15.76],
[4.161, 1.964, 36.63],
[5.747, 1.958, 22.55],
[7.912, 1.361, 81.04]])

# 3) Nanosphere + Na2
classical_material = PolarizableMaterial()
permittivity=eps_gold))

# Combined Poisson solver
poissonsolver = FDTDPoissonSolver(classical_material=classical_material,
qm_spacing=0.5,
cl_spacing=2.0,
cell=simulation_cell,
communicator=world,
remove_moments=(1, 1))
poissonsolver.set_calculation_mode('iterate')

# Combined system
atoms.set_cell(simulation_cell)
atoms, qm_spacing, gpts = poissonsolver.cut_cell(atoms, vacuum=4.0)

# Initialize GPAW
gs_calc = GPAW(gpts=gpts,
nbands=-1,
poissonsolver=poissonsolver)
atoms.calc = gs_calc

# Ground state
energy = atoms.get_potential_energy()

# Save state
gs_calc.write('gs.gpw', 'all')

# Initialize TDDFT and FDTD
kick = [0.001, 0.000, 0.000]
time_step = 10
iterations = 1500

td_calc = TDDFT('gs.gpw')
DipoleMomentWriter(td_calc, 'dm.dat')

# Attach InducedFields to the calculation
frequencies = [2.05, 2.60]
width = 0.15
cl_ind = FDTDInducedField(paw=td_calc,
frequencies=frequencies,
width=width)
qm_ind = TDDFTInducedField(paw=td_calc,
frequencies=frequencies,
width=width)

# Propagate TDDFT and FDTD
td_calc.absorption_kick(kick_strength=kick)
td_calc.propagate(time_step, iterations)

# Save results
td_calc.write('td.gpw', 'all')
cl_ind.write('cl.ind')
qm_ind.write('qm.ind')

photoabsorption_spectrum('dm.dat', 'spec.3.dat', width=width)


The TDDFTInducedField records the quantum part of the calculation and the FDTDInducedField records the classical part. We can calculate the individual and the total induced field by the following script:

from gpaw.tddft import TDDFT
from gpaw.inducedfield.inducedfield_fdtd import (
FDTDInducedField, calculate_hybrid_induced_field)
from gpaw.inducedfield.inducedfield_tddft import TDDFTInducedField

td_calc = TDDFT('td.gpw')

# Classical subsystem
cl_ind = FDTDInducedField(filename='cl.ind', paw=td_calc)
cl_ind.calculate_induced_field(gridrefinement=2)
cl_ind.write('cl_field.ind', mode='all')

# Quantum subsystem
qm_ind = TDDFTInducedField(filename='qm.ind', paw=td_calc)
qm_ind.calculate_induced_field(gridrefinement=2)
qm_ind.write('qm_field.ind', mode='all')

# Total system, interpolate/extrapolate to a grid with spacing h
tot_ind = calculate_hybrid_induced_field(cl_ind, qm_ind, h=1.0)
tot_ind.write('tot_field.ind', mode='all')


All the InducedField objects can be analyzed in the same way as described in TDDFTInducedField module documentation. Here we show an example script for plotting (run in serial mode, i.e., with one process):

# web-page: cl_field.ind_Ffe.png, qm_field.ind_Ffe.png, tot_field.ind_Ffe.png
from gpaw.mpi import world

import numpy as np
import matplotlib.pyplot as plt

from gpaw.inducedfield.inducedfield_base import BaseInducedField
from gpaw.tddft.units import aufrequency_to_eV

assert world.size == 1, 'This script should be run in serial mode.'

# Helper function
def do_plot(d_g, ng, box, atoms):
# Take slice of data array
d_yx = d_g[:, :, ng // 2]
y = np.linspace(0, box, ng + 1)[:-1]
dy = box / (ng + 1)
y += dy * 0.5
ylabel = u'x / Å'
x = np.linspace(0, box, ng + 1)[:-1]
dx = box / (ng + 1)
x += dx * 0.5
xlabel = u'y / Å'

# Plot
plt.figure()
ax = plt.subplot(1, 1, 1)
X, Y = np.meshgrid(x, y)
plt.contourf(X, Y, d_yx, 40)
plt.colorbar()
for atom in atoms:
pos = atom.position
plt.scatter(pos, pos, s=50, c='k', marker='o')
plt.xlabel(xlabel)
plt.ylabel(ylabel)
plt.xlim([x, x[-1]])
plt.ylim([y, y[-1]])
ax.set_aspect('equal')

for fname, name in zip(['cl_field.ind', 'qm_field.ind', 'tot_field.ind'],
['Classical subsystem', 'Quantum subsystem',
'Total hybrid system']):

# Choose array
w = 0                                      # Frequency index
freq = ind.omega_w[w] * aufrequency_to_eV  # Frequency
box = np.diag(ind.atoms.get_cell())        # Calculation box
d_g = ind.Ffe_wg[w]                        # Data array
ng = d_g.shape                             # Size of grid
atoms = ind.atoms                          # Atoms

do_plot(d_g, ng, box, atoms)
plt.title(f'{name}\nField enhancement @ {freq:.2f} eV')
plt.savefig(fname + '_Ffe.png', bbox_inches='tight')

# Imaginary part of density
d_g = ind.Frho_wg[w].imag
ng = d_g.shape
do_plot(d_g, ng, box, atoms)
plt.title('%s\nImaginary part of induced charge density @ %.2f eV' %
(name, freq))
plt.savefig(fname + '_Frho.png', bbox_inches='tight')

# Imaginary part of potential
d_g = ind.Fphi_wg[w].imag
ng = d_g.shape
do_plot(d_g, ng, box, atoms)
plt.title(f'{name}\nImaginary part of induced potential @ {freq:.2f} eV')
plt.savefig(fname + '_Fphi.png', bbox_inches='tight')


This produces the following figures for the electric near field: