import numpy as np
import veloxchem as vlxOrder expansion of parameters¶
The wave function parameters are expanded in orders of the external field
such that in the limit of .
Conversely,
Linear response equation¶
The first-order response can be determined from the field-derivative of the variational condition
We here view the -pair of unoccupied/occupied orbital indices as a compound index (and similarly for and ), inviting us to introduce a compact matrix notation to arrive at the linear response equation.
Baker–Campbell–Hausdorff expansion¶
We have
and let us adopt the electric-dipole approximation
It is then straightforward to evaluate that electronic Hessian and the property gradient. We get
and
Spin-adapted excitation operators¶
For closed-shell reference states in VeloxChem, we are using normalized spin-adapted excitation operators that will result in
and
and, in this case, the block dimension equals
h2o_xyz = """3
O 0.000000000000 0.000000000000 0.000000000000
H 0.000000000000 0.740848095288 0.582094932012
H 0.000000000000 -0.740848095288 0.582094932012
"""scf_drv = vlx.ScfRestrictedDriver()
scf_drv.ostream.mute()
molecule = vlx.Molecule.read_xyz_string(h2o_xyz)
basis = vlx.MolecularBasis.read(molecule, "6-31g", ostream=None)
scf_results = scf_drv.compute(molecule, basis)epsilon = scf_results["E_alpha"] # orbital energieserimo_drv = vlx.MOIntegralsDriver() # ERI blocks in the MO basis
ovov = erimo_drv.compute_in_memory(molecule, basis, scf_drv.mol_orbs, "chem_ovov")
oovv = erimo_drv.compute_in_memory(molecule, basis, scf_drv.mol_orbs, "chem_oovv")
print("(ov|ov):", ovov.shape)
print("(oo|vv):", oovv.shape)(ov|ov): (5, 8, 5, 8)
(oo|vv): (5, 5, 8, 8)
nocc = molecule.number_of_alpha_electrons()
norb = basis.get_dimension_of_basis()
nvirt = norb - nocc
ndim = nocc * nvirt
print("Number of occupied orbitals:", nocc)
print("Number of virtual orbitals :", nvirt)
print("Block dimension :", ndim)Number of occupied orbitals: 5
Number of virtual orbitals : 8
Block dimension : 40
Electronic Hessian¶
We determine the electronic Hessian
def delta(i, j):
if i == j:
delta = 1
else:
delta = 0
return deltaE2 = np.zeros((2 * ndim, 2 * ndim)) # real valued
A = np.zeros((ndim, ndim))
B = np.zeros((ndim, ndim))
m = 0
for j in range(nocc):
for b in range(nvirt):
n = 0
for i in range(nocc):
for a in range(nvirt):
A[m, n] = (
(epsilon[a + nocc] - epsilon[i]) * delta(a, b) * delta(i, j)
+ 2 * ovov[j, b, i, a]
- oovv[j, i, b, a]
)
B[m, n] = -2 * ovov[j, b, i, a] + ovov[j, a, i, b]
n += 1
m += 1
E2[:ndim, :ndim] = A
E2[:ndim, ndim:] = B
E2[ndim:, :ndim] = B
E2[ndim:, ndim:] = ATwo observations can be made:
matrix block is diagonal dominant with diagonal elements approximately equal to orbital energy differences (in particular for core electron transitions)
elements of are comparatively small
core, homo, lumo = 0, 4, 5 # Python indexing of orbitals
print(f"{'1s-LUMO':>48s}{'HOMO-LUMO':>17s}")
print(" " * 41 + "=" * 25)
print(
"Diagonal elements of A-block of E2 :"
+ f"{A[0,0]:14.8f}"
+ f"{A[(nocc-1)*nvirt,(nocc-1)*nvirt]:14.8f}"
)
print(
"Associated orbital energy differences:"
+ f"{epsilon[lumo] - epsilon[core]:14.8f}"
+ f"{epsilon[lumo] - epsilon[homo]:14.8f}\n"
)
print(f"Maximum element of B-block of E2 : {np.max(B):12.8f}") 1s-LUMO HOMO-LUMO
=========================
Diagonal elements of A-block of E2 : 20.34024945 0.36669368
Associated orbital energy differences: 20.76493334 0.70960177
Maximum element of B-block of E2 : 0.21128143
We ensure that our result equals the electronic Hessian obtained with the get_e2() method of the LinearResponseEigenSolver class.
lres_drv = vlx.LinearResponseEigenSolver()
lres_drv.ostream.mute()
E2_ref = lres_drv.get_e2(molecule, basis, scf_results)np.testing.assert_allclose(E2, E2_ref, atol=1.0e-6)Property gradient¶
dipole_mats = vlx.compute_electric_dipole_integrals(molecule, basis)
mu_z_ao = dipole_mats[2]
C = scf_results["C_alpha"]
mu_z = np.einsum("ap, ab, bq -> pq", C, mu_z_ao, C)B1 = np.zeros(2 * ndim, dtype=complex) # imaginary valued
g = []
for j in range(nocc):
for b in range(nocc, norb):
g.append(1j * np.sqrt(2) * mu_z[j, b])
g = np.array(g)
B1[:ndim] = g
B1[ndim:] = g.conj()We ensure that our result equals the reference property gradient obtained with the get_prop_grad() method of the LinearResponseSolver class.
lrs_drv = vlx.LinearResponseSolver()
lrs_drv.ostream.mute()
B1_ref = lrs_drv.get_prop_grad(
"electric_dipole", "z", molecule, basis, scf_drv.scf_tensors
)[0]np.testing.assert_allclose(B1.imag, B1_ref, atol=1.0e-12)N = -np.matmul(np.linalg.inv(E2), B1)We ensure that our result equals the reference response vector obtained with the get_full_vector() method.
lrs_drv.a_operator = "electric dipole"
lrs_drv.b_operator = "electric dipole"
lrs_drv.a_components = ["z"]
lrs_drv.b_components = ["z"]
lrs_drv.frequencies = [0.0]
lrs_results = lrs_drv.compute(molecule, basis, scf_results)N_ref = np.zeros(2 * ndim)
N_ref[:ndim] = -lrs_results["solutions"][("z", 0.0)].get_full_vector(1)
N_ref[ndim:] = lrs_results["solutions"][("z", 0.0)].get_full_vector(1)np.testing.assert_allclose(N.imag, N_ref, atol=1.0e-6)We note that the imaginary “” is left out in the response vector provided by VeloxChem.
A1 = B1 # property gradient of the observable
print(f"Response function: {np.dot(A1.conj(), N).real:10.6f}")Response function: -4.298489
print(f"Reference value: {lrs_results['response_functions'][('z', 'z', 0.0)]:10.6f}")Reference value: -4.298489
We note that our result is in perfect agreement with the reference value obtained from the VeloxChem program.