From 67ac50beb5e52e65b7bac16e16cc71d4413fce54 Mon Sep 17 00:00:00 2001
From: puzhichen <147788878+puzhichen@users.noreply.github.com>
Date: Wed, 15 May 2024 14:13:27 +0800
Subject: [PATCH] Properties dev (#147)
* Add property calculation codes, including NMR shielding constants, polarizability and IR intensity.
* Add the polarizability's unittest. NMR needs DEBUGGING!
* Clean the NMR codes and add a unittest.
* Add some comments to unit tests.
* Write the unit test for IR intensity.
* Add a test for ir intensity WIP
* Add the unit test for IR, and remove some comments.
* Fix some typos
* Fix some instandard codes.
---
gpu4pyscf/properties/__init__.py | 16 ++
gpu4pyscf/properties/ir.py | 129 +++++++++
gpu4pyscf/properties/polarizability.py | 92 ++++++
gpu4pyscf/properties/shielding.py | 263 ++++++++++++++++++
.../properties/tests/test_ir_intensity.py | 101 +++++++
.../properties/tests/test_polarizability.py | 128 +++++++++
gpu4pyscf/properties/tests/test_shielding.py | 140 ++++++++++
7 files changed, 869 insertions(+)
create mode 100644 gpu4pyscf/properties/__init__.py
create mode 100644 gpu4pyscf/properties/ir.py
create mode 100644 gpu4pyscf/properties/polarizability.py
create mode 100644 gpu4pyscf/properties/shielding.py
create mode 100644 gpu4pyscf/properties/tests/test_ir_intensity.py
create mode 100644 gpu4pyscf/properties/tests/test_polarizability.py
create mode 100644 gpu4pyscf/properties/tests/test_shielding.py
diff --git a/gpu4pyscf/properties/__init__.py b/gpu4pyscf/properties/__init__.py
new file mode 100644
index 00000000..cf49865c
--- /dev/null
+++ b/gpu4pyscf/properties/__init__.py
@@ -0,0 +1,16 @@
+# Copyright 2023 The GPU4PySCF Authors. All Rights Reserved.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from gpu4pyscf.properties import polarizability, ir, shielding
\ No newline at end of file
diff --git a/gpu4pyscf/properties/ir.py b/gpu4pyscf/properties/ir.py
new file mode 100644
index 00000000..9f813df9
--- /dev/null
+++ b/gpu4pyscf/properties/ir.py
@@ -0,0 +1,129 @@
+# Copyright 2023 The GPU4PySCF Authors. All Rights Reserved.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from functools import reduce
+from pyscf.hessian import thermo
+import numpy as np
+import cupy
+from pyscf.data import elements, nist
+from scipy.constants import physical_constants
+from gpu4pyscf.lib import logger
+from gpu4pyscf.lib.cupy_helper import contract
+
+LINDEP_THRESHOLD = 1e-7
+
+
+def eval_ir_freq_intensity(mf, hessian_obj):
+ """main function to calculate the polarizability
+
+ Args:
+ mf: mean field object
+ unit (str, optional): the unit of the polarizability. Defaults to 'au'.
+
+ Returns:
+ polarizability (numpy.array): polarizability
+ """
+ log = logger.new_logger(hessian_obj, mf.mol.verbose)
+ hessian = hessian_obj.kernel()
+ hartree_kj = nist.HARTREE2J*1e3
+ unit2cm = ((hartree_kj * nist.AVOGADRO)**.5 / (nist.BOHR*1e-10)
+ / (2*np.pi*nist.LIGHT_SPEED_SI) * 1e-2)
+ natm = mf.mol.natm
+ nao = mf.mol.nao
+ dm0 = mf.make_rdm1()
+
+ atom_charges = mf.mol.atom_charges()
+ mass = cupy.array([elements.MASSES[atom_charges[i]] for i in range(natm)])
+ # hessian_mass = contract('ijkl,i,j->ijkl', hessian,
+ # 1/cupy.sqrt(mass), 1/cupy.sqrt(mass))
+ hessian_mass = contract('ijkl,i->ijkl', cupy.array(hessian), 1/cupy.sqrt(mass))
+ hessian_mass = contract('ijkl,j->ijkl', hessian_mass, 1/cupy.sqrt(mass))
+
+ TR = thermo._get_TR(mass.get(), mf.mol.atom_coords())
+ TRspace = []
+ TRspace.append(TR[:3])
+
+ rot_const = thermo.rotation_const(mass.get(), mf.mol.atom_coords())
+ rotor_type = thermo._get_rotor_type(rot_const)
+ if rotor_type == 'ATOM':
+ pass
+ elif rotor_type == 'LINEAR': # linear molecule
+ TRspace.append(TR[3:5])
+ else:
+ TRspace.append(TR[3:])
+
+ if TRspace:
+ TRspace = cupy.vstack(TRspace)
+ q, r = cupy.linalg.qr(TRspace.T)
+ P = cupy.eye(natm * 3) - q.dot(q.T)
+ w, v = cupy.linalg.eigh(P)
+ bvec = v[:,w > LINDEP_THRESHOLD]
+ h = reduce(cupy.dot, (bvec.T, hessian_mass.transpose(0,2,1,3).reshape(3*natm,3*natm), bvec))
+ e, mode = cupy.linalg.eigh(h)
+ mode = bvec.dot(mode)
+
+ c = contract('ixn,i->ixn', mode.reshape(natm, 3, -1),
+ 1/np.sqrt(mass)).reshape(3*natm, -1)
+ freq = cupy.sign(e)*cupy.sqrt(cupy.abs(e))*unit2cm
+
+ mo_coeff = mf.mo_coeff
+ mo_occ = mf.mo_occ
+ mo_energy = mf.mo_energy
+ mo_coeff = cupy.array(mo_coeff)
+ mo_occ = cupy.array(mo_occ)
+ mo_energy = cupy.array(mo_energy)
+ mocc = mo_coeff[:, mo_occ > 0]
+ mocc = cupy.array(mocc)
+
+ atmlst = range(natm)
+ h1ao = hessian_obj.make_h1(mo_coeff, mo_occ, None, atmlst)
+ # TODO: compact with hessian method, which can save one time cphf solve.
+ # ! Different from PySCF, mo1 is all in mo!
+ mo1, mo_e1 = hessian_obj.solve_mo1(mo_energy, mo_coeff, mo_occ, h1ao,
+ None, atmlst, hessian_obj.max_memory, log)
+
+
+ tmp = cupy.empty((3, 3, natm)) # dipole moment, x,y,z
+ aoslices = mf.mol.aoslice_by_atom()
+ with mf.mol.with_common_orig((0, 0, 0)):
+ hmuao = mf.mol.intor('int1e_r') # mu
+ hmuao11 = -mf.mol.intor('int1e_irp').reshape(3, 3, nao, nao)
+ hmuao = cupy.array(hmuao)
+ hmuao11 = cupy.array(hmuao11)
+ for i0, ia in enumerate(atmlst):
+ shl0, shl1, p0, p1 = aoslices[ia]
+ h11ao = cupy.zeros((3, 3, nao, nao))
+
+ h11ao[:, :, :, p0:p1] += hmuao11[:, :, :, p0:p1]
+ h11ao[:, :, p0:p1] += hmuao11[:, :, :, p0:p1].transpose(0, 1, 3, 2)
+
+ tmp0 = contract('ypi,vi->ypv', mo1[ia], mocc) # nabla
+ dm1 = contract('ypv,up->yuv', tmp0, mo_coeff)
+ tmp[:, :, ia] = -contract('xuv,yuv->xy', hmuao, dm1) * 4 #the minus means the density should be negative, but calculate it is positive.
+ tmp[:, :, ia] -= contract('xyuv,vu->xy', h11ao, dm0)
+ tmp[:, :, ia] += mf.mol.atom_charge(ia)*cupy.eye(3)
+
+ alpha = physical_constants["fine-structure constant"][0]
+ amu = physical_constants["atomic mass constant"][0]
+ m_e = physical_constants["electron mass"][0]
+ N_A = physical_constants["Avogadro constant"][0]
+ a_0 = physical_constants["Bohr radius"][0]
+ unit_kmmol = alpha**2 * (1e-3 / amu) * m_e * N_A * np.pi * a_0 / 3
+
+ intensity = contract('xym,myn->xn', tmp, c.reshape(natm, 3, -1))
+ intensity = contract('xn,xn->n', intensity, intensity)
+ intensity *= unit_kmmol
+
+ return freq, intensity
diff --git a/gpu4pyscf/properties/polarizability.py b/gpu4pyscf/properties/polarizability.py
new file mode 100644
index 00000000..567f71c1
--- /dev/null
+++ b/gpu4pyscf/properties/polarizability.py
@@ -0,0 +1,92 @@
+# Copyright 2023 The GPU4PySCF Authors. All Rights Reserved.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import numpy as np
+from gpu4pyscf.scf import cphf
+import cupy
+from gpu4pyscf.lib.cupy_helper import contract
+
+
+def gen_vind(mf, mo_coeff, mo_occ):
+ """get the induced potential. This is the same as contract the mo1 with the kernel.
+
+ Args:
+ mf: mean field object
+ mo_coeff (numpy.array): mo coefficients
+ mo_occ (numpy.array): mo_coefficients
+
+ Returns:
+ fx (function): a function to calculate the induced potential with the input as the mo1.
+ """
+ nao, nmo = mo_coeff.shape
+ mocc = mo_coeff[:, mo_occ > 0]
+ mvir = mo_coeff[:, mo_occ == 0]
+ nocc = mocc.shape[1]
+ nvir = nmo - nocc
+ vresp = mf.gen_response(mo_coeff, mo_occ, hermi=1)
+
+ def fx(mo1):
+ mo1 = mo1.reshape(-1, nvir, nocc) # * the saving pattern
+ mo1_mo_real = contract('nai,ua->nui', mo1, mvir)
+ dm1 = 2*contract('nui,vi->nuv', mo1_mo_real, mocc.conj())
+ dm1+= dm1.transpose(0,2,1)
+
+ v1 = vresp(dm1) # (nset, nao, nao)
+ tmp = contract('nuv,vi->nui', v1, mocc)
+ v1vo = contract('nui,ua->nai', tmp, mvir.conj())
+
+ return v1vo
+ return fx
+
+
+def eval_polarizability(mf, unit='au'):
+ """main function to calculate the polarizability
+
+ Args:
+ mf: mean field object
+ unit (str, optional): the unit of the polarizability. Defaults to 'au'.
+
+ Returns:
+ polarizability (numpy.array): polarizability
+ """
+
+ polarizability = np.empty((3, 3))
+
+ mo_coeff = mf.mo_coeff
+ mo_occ = mf.mo_occ
+ mo_energy = mf.mo_energy
+ mo_coeff = cupy.array(mo_coeff)
+ mo_occ = cupy.array(mo_occ)
+ mo_energy = cupy.array(mo_energy)
+ fx = gen_vind(mf, mo_coeff, mo_occ)
+ mocc = mo_coeff[:, mo_occ > 0]
+ mvir = mo_coeff[:, mo_occ == 0]
+
+ with mf.mol.with_common_orig((0, 0, 0)):
+ h1 = mf.mol.intor('int1e_r')
+ h1 = cupy.array(h1)
+ for idirect in range(3):
+ h1ai = -mvir.T.conj()@h1[idirect]@mocc
+ mo1 = cphf.solve(fx, mo_energy, mo_occ, h1ai, max_cycle=20, tol=1e-10)[0]
+ for jdirect in range(idirect, 3):
+ p10 = np.trace(mo1.conj().T@mvir.conj().T@h1[jdirect]@mocc)*2
+ p01 = np.trace(mocc.conj().T@h1[jdirect]@mvir@mo1)*2
+ polarizability[idirect, jdirect] = p10+p01
+ polarizability[1, 0] = polarizability[0, 1]
+ polarizability[2, 0] = polarizability[0, 2]
+ polarizability[2, 1] = polarizability[1, 2]
+
+ return polarizability
+
diff --git a/gpu4pyscf/properties/shielding.py b/gpu4pyscf/properties/shielding.py
new file mode 100644
index 00000000..dbbeccc6
--- /dev/null
+++ b/gpu4pyscf/properties/shielding.py
@@ -0,0 +1,263 @@
+# Copyright 2023 The GPU4PySCF Authors. All Rights Reserved.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from gpu4pyscf.scf import cphf
+import numpy as np
+from pyscf.data import nist
+import cupy
+from pyscf.scf import _vhf, jk
+from gpu4pyscf.dft import numint
+from gpu4pyscf.lib.cupy_helper import contract, take_last2d, add_sparse
+
+
+def gen_vind(mf, mo_coeff, mo_occ):
+ """get the induced potential. This is the same as contract the mo1 with the kernel.
+
+ Args:
+ mf: mean field object
+ mo_coeff (numpy.array): mo coefficients
+ mo_occ (numpy.array): mo_coefficients
+
+ Returns:
+ fx (function): a function to calculate the induced potential with the input as the mo1.
+ """
+ nao, nmo = mo_coeff.shape
+ mocc = mo_coeff[:, mo_occ > 0]
+ mvir = mo_coeff[:, mo_occ == 0]
+ nocc = mocc.shape[1]
+ nvir = nmo - nocc
+ omega, alpha, hyb = mf._numint.rsh_and_hybrid_coeff(
+ mf.xc, spin=mf.mol.spin)
+
+ def fx(mo1):
+ mo1 = mo1.reshape(-1, nvir, nocc) # * the saving pattern
+ mo1_mo_real = contract('nai,ua->nui', mo1, mvir)
+ dm1 = 2*contract('nui,vi->nuv', mo1_mo_real, mocc.conj())
+ dm1 -= dm1.transpose(0, 2, 1)
+ if hasattr(mf,'with_df'):
+ v1 = cupy.empty((3, nao, nao))
+ for i in range(3):
+ v1[i] =+mf.get_jk(mf.mol, dm1[i], hermi=2, with_j=False)[1]*0.5*hyb
+ else:
+ v1 = np.empty((3, nao, nao))
+ for i in range(3):
+ v1[i] = -jk.get_jk(mf.mol, dm1[i].get(), 'ijkl,jk->il')*0.5*hyb
+ v1 = cupy.array(v1)
+ tmp = contract('nuv,vi->nui', v1, mocc)
+ v1vo = contract('nui,ua->nai', tmp, mvir.conj())
+
+ return v1vo
+
+ return fx
+
+
+def nr_rks(ni, mol, grids, xc_code, dms):
+
+ xctype = ni._xc_type(xc_code)
+ mo_coeff = getattr(dms, 'mo_coeff', None)
+ mo_occ = getattr(dms, 'mo_occ', None)
+ nao = mo_coeff.shape[1]
+
+ opt = getattr(ni, 'gdftopt', None)
+ if opt is None:
+ ni.build(mol, grids.coords)
+ opt = ni.gdftopt
+ coeff = cupy.asarray(opt.coeff)
+ nao, nao0 = coeff.shape
+ dms = cupy.asarray(dms).reshape(-1,nao0,nao0)
+ dms = take_last2d(dms, opt.ao_idx)
+ mo_coeff = mo_coeff[opt.ao_idx]
+
+ vmat = cupy.zeros((3, nao, nao))
+ if xctype == 'LDA':
+ ao_deriv = 0
+ else:
+ ao_deriv = 1
+
+ for ao, index, weight, coords in ni.block_loop(opt.mol, grids, nao, ao_deriv):
+ mo_coeff_mask = mo_coeff[index,:]
+ rho = numint.eval_rho2(opt.mol, ao, mo_coeff_mask, mo_occ, None, xctype)
+ vxc = ni.eval_xc_eff(xc_code, rho, deriv=1, xctype=xctype)[1]
+ if xctype == 'LDA':
+ wv = weight * vxc[0]
+ giao = opt.mol.eval_gto('GTOval_ig', coords.get(), comp=3) # (#C(0 1) g) |AO>
+ giao = cupy.array(giao)
+ giao_aux = giao[:,:,index]
+ for idirect in range(3):
+ vtmp = contract('pu,p,vp->uv', giao_aux[idirect], wv, ao)
+ vtmp = cupy.ascontiguousarray(vtmp)
+ add_sparse(vmat[idirect], vtmp, index)
+
+ elif xctype == 'GGA':
+ wv = vxc * weight
+ giao = opt.mol.eval_gto('GTOval_ig', coords.get(), comp=3)
+ giao_nabla = opt.mol.eval_gto('GTOval_ipig', coords.get()).reshape(3, 3, -1, nao)
+ giao = cupy.array(giao)
+ giao_nabla = cupy.array(giao_nabla)
+ giao_aux = giao[:,:,index]
+ giao_nabla_aux = giao_nabla[:,:,:,index]
+ for idirect in range(3):
+ # * write like the gpu4pyscf numint part, but explicitly use the einsum
+ aow = contract('pn,p->pn', giao_aux[idirect], wv[0])
+ aow += contract('xpn,xp->pn', giao_nabla_aux[:, idirect, :, :], wv[1:4])
+ vtmp = contract('pn,mp->nm', aow, ao[0])
+ vtmp = cupy.ascontiguousarray(vtmp)
+ add_sparse(vmat[idirect], vtmp, index)
+ aow = contract('pn,xp->xpn', giao_aux[idirect], wv[1:4])
+ vtmp = contract('xpn,xmp->nm', aow, ao[1:4])
+ vtmp = cupy.ascontiguousarray(vtmp)
+ add_sparse(vmat[idirect], vtmp, index)
+
+ elif xctype == 'NLC':
+ raise NotImplementedError('NLC')
+ elif xctype == 'MGGA':
+ raise NotImplementedError('Meta-GGA')
+ elif xctype == 'HF':
+ pass
+ else:
+ raise NotImplementedError(
+ f'numint.nr_rks for functional {xc_code}')
+
+ ao = None
+
+ # vmat = contract('pi,npq->niq', coeff, vmat)
+ # vmat = contract('qj,niq->nij', coeff, vmat)
+ vmat = take_last2d(vmat, opt.rev_ao_idx)
+
+ if numint.FREE_CUPY_CACHE:
+ dms = None
+ cupy.get_default_memory_pool().free_all_blocks()
+
+ return (vmat - vmat.transpose(0, 2, 1))
+
+
+# TODO: this can be modified into jk, not _vhf
+def get_jk(mol, dm0):
+ # J = Im[(i i|\mu g\nu) + (i gi|\mu \nu)] = -i (i i|\mu g\nu)
+ # K = Im[(\mu gi|i \nu) + (\mu i|i g\nu)]
+ # = [-i (\mu g i|i \nu)] - h.c. (-h.c. for anti-symm because of the factor -i)
+ intor = mol._add_suffix('int2e_ig1')
+ vj, vk = _vhf.direct_mapdm(intor, # (g i,j|k,l)
+ 'a4ij', ('lk->s1ij', 'jk->s1il'),
+ dm0.get(), 3, # xyz, 3 components
+ mol._atm, mol._bas, mol._env)
+ vk = vk - np.swapaxes(vk, -1, -2)
+ return -cupy.array(vj), -cupy.array(vk)
+
+
+def get_vxc(mf, dm0):
+ vxc = nr_rks(mf._numint, mf.mol, mf.grids, mf.xc, mf.make_rdm1())
+ # ! imaginary part
+ vj, vk = get_jk(mf.mol, dm0)
+ if not mf._numint.libxc.is_hybrid_xc(mf.xc):
+ vk = None
+ vxc += vj
+ else:
+ omega, alpha, hyb = mf._numint.rsh_and_hybrid_coeff(
+ mf.xc, spin=mf.mol.spin)
+ vxc += vj - vk*hyb*0.5
+ return vxc
+
+
+def get_h1ao(mf):
+ dm0 = mf.make_rdm1()
+ # ! imaginary part
+ h1ao = -0.5*mf.mol.intor('int1e_giao_irjxp')
+ h1ao += -mf.mol.intor('int1e_igkin')
+ h1ao += -mf.mol.intor('int1e_ignuc')
+ h1ao = cupy.array(h1ao)
+ h1ao += get_vxc(mf, dm0)
+
+ return h1ao
+
+
+def eval_shielding(mf):
+
+ mo_coeff = mf.mo_coeff
+ mo_occ = mf.mo_occ
+ mo_energy = mf.mo_energy
+ mo_coeff = cupy.array(mo_coeff)
+ mo_occ = cupy.array(mo_occ)
+ mo_energy = cupy.array(mo_energy)
+
+ nao = mo_energy.shape[0]
+ idx_occ = mo_occ > 0
+ idx_vir = mo_occ == 0
+ fx = gen_vind(mf, mo_coeff, mo_occ)
+ mocc = mo_coeff[:, idx_occ]
+ mvir = mo_coeff[:, idx_vir]
+ s1ao = -mf.mol.intor('int1e_igovlp')
+ s1ao = cupy.array(s1ao)
+ dm0 = mf.make_rdm1()
+ natom = mf.mol.natm
+
+ h1ao = get_h1ao(mf)
+ tmp = contract('xuv,ua->xav', s1ao, mvir)
+ s1ai = contract('xav,vi->xai', tmp, mocc)
+ tmp = contract('ned,ue->nud', s1ao, dm0)
+ s1dm = contract('nud,dv->nuv', tmp, dm0)*0.5
+ tmp = contract('xpq,pi->xiq', s1ao, mocc)
+ s1jk = -contract('xiq,qj->xij', tmp, mocc)*0.5
+ tmp = contract('nai,ua->nui', s1jk, mocc)
+ s1jkdm1 = contract('nui,vi->nuv', tmp, mocc.conj())*2
+ s1jkdm1 -= s1jkdm1.transpose(0, 2, 1)
+ omega, alpha, hyb = mf._numint.rsh_and_hybrid_coeff(
+ mf.xc, spin=mf.mol.spin)
+ if hasattr(mf,'with_df'):
+ vk2 = cupy.empty((3, nao, nao))
+ for i in range(3):
+ vk2[i] = +mf.get_jk(mf.mol, s1jkdm1[i], hermi=2, with_j=False)[1]*0.5*hyb
+
+ else:
+ vk2 = np.empty((3, nao, nao))
+ for i in range(3):
+ vk2[i] = -jk.get_jk(mf.mol, s1jkdm1[i].get(), 'ijkl,jk->il')*0.5*hyb
+ vk2 = cupy.array(vk2)
+ h1ao += vk2
+ tmp = contract('xuv,ua->xav', h1ao, mvir)
+ Veff_ai = contract('xav,vi->xai', tmp, mocc)
+ Veff_ai -= contract('xai,i->xai', s1ai, mo_energy[idx_occ])
+ Veff_ai = cupy.array(Veff_ai)
+ mo1 = cphf.solve(fx, mo_energy, mo_occ, Veff_ai, max_cycle=20, tol=1e-10)[0]
+
+ shielding_d = cupy.empty((natom, 3, 3))
+ shielding_p = cupy.empty((natom, 3, 3))
+
+ for atm_id in range(natom):
+ mf.mol.set_rinv_origin(mf.mol.atom_coord(atm_id))
+ # ! imaginary part (p* | rinv cross p | )
+ int_h01 = -mf.mol.intor('int1e_prinvxp')
+ # ! imaginary part (g | nabla-rinv cross p |)
+ int_h01_giao = mf.mol.intor('int1e_a01gp').reshape(
+ 3, 3, nao, nao)
+ int_h11 = mf.mol.intor('int1e_giao_a11part').reshape(
+ 3, 3, nao, nao) # ! (-.5 | nabla-rinv | r)
+ int_h11 = cupy.array(int_h11)
+ int_h01 = cupy.array(int_h01)
+ int_h01_giao = cupy.array(int_h01_giao)
+ int_h11_diag = int_h11[0, 0] + int_h11[1, 1] + int_h11[2, 2]
+ int_h11[0, 0] -= int_h11_diag
+ int_h11[1, 1] -= int_h11_diag
+ int_h11[2, 2] -= int_h11_diag
+
+ tmp = contract('ua,xai->xui', mvir, mo1)
+ dm1 = contract('xui,vi->xvu', tmp, mocc)
+ dm1 *= 2
+ shielding_p[atm_id] = contract('yuv,xvu->xy', int_h01, dm1)*2.0
+ shielding_p[atm_id] += contract('yuv,xvu->xy', int_h01, s1dm)
+ shielding_d[atm_id] = contract('xyuv,vu->xy', int_h11, dm0)
+ shielding_d[atm_id] += contract('xyuv,vu->xy', int_h01_giao, dm0)
+ ppm = nist.ALPHA**2 * 1e6
+ return shielding_d*ppm, shielding_p*ppm
diff --git a/gpu4pyscf/properties/tests/test_ir_intensity.py b/gpu4pyscf/properties/tests/test_ir_intensity.py
new file mode 100644
index 00000000..8614b588
--- /dev/null
+++ b/gpu4pyscf/properties/tests/test_ir_intensity.py
@@ -0,0 +1,101 @@
+# Copyright 2023 The GPU4PySCF Authors. All Rights Reserved.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import unittest
+import numpy as np
+import pyscf
+from pyscf import lib
+from gpu4pyscf.dft import rks, uks
+from gpu4pyscf.properties import ir
+
+lib.num_threads(8)
+
+atom = '''
+O 0.0000000000 -0.0000000000 0.1174000000
+H -0.7570000000 -0.0000000000 -0.4696000000
+H 0.7570000000 0.0000000000 -0.4696000000
+'''
+
+bas='def2tzvpp'
+grids_level = 6
+
+def setUpModule():
+ global mol
+ mol = pyscf.M(atom=atom, basis=bas, max_memory=32000)
+ mol.output = '/dev/null'
+ mol.build()
+ mol.verbose = 1
+
+def tearDownModule():
+ global mol
+ mol.stdout.close()
+ del mol
+
+def run_dft_df_if(xc):
+ mf = rks.RKS(mol, xc=xc).density_fit(auxbasis='def2-universal-jkfit')
+ mf.grids.level = grids_level
+ mf.conv_tol_cpscf = 1e-3
+ e_dft = mf.kernel()
+ h = mf.Hessian()
+ h.auxbasis_response = 2
+ freq, intensity = ir.eval_ir_freq_intensity(mf, h)
+ return e_dft, freq, intensity
+
+
+class KnownValues(unittest.TestCase):
+ '''
+ known values are obtained by Q-Chem
+
+ $rem
+ MEM_STATIC 2000
+ JOBTYPE freq
+ METHOD b3lyp
+ BASIS def2-tzvpp
+ SCF_CONVERGENCE 12
+ XC_GRID 000099000590
+ SYMMETRY FALSE
+ SYM_IGNORE = TRUE
+ ri_j True
+ ri_k True
+ aux_basis RIJK-def2-tzvpp
+ THRESH 14
+ $end
+
+ <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
+ Mode: 1 2 3
+ Frequency: 1630.86 3850.08 3949.35
+ Force Cnst: 1.6970 9.1261 9.9412
+ Red. Mass: 1.0829 1.0450 1.0818
+ IR Active: YES YES YES
+ IR Intens: 69.334 4.675 47.943
+ Raman Active: YES YES YES
+
+ '''
+
+ def test_rks_b3lyp_df(self):
+ print('-------- RKS density fitting B3LYP -------------')
+ e_tot, freq, intensity = run_dft_df_if('B3LYP')
+ assert np.allclose(e_tot, -76.4666819537)
+ qchem_freq = np.array([1630.86, 3850.08, 3949.35])
+ qchem_intensity = np.array([69.334, 4.675, 47.943])
+ print(freq, intensity)
+ assert np.allclose(freq, qchem_freq, rtol=1e-03)
+ assert np.allclose(intensity, qchem_intensity, rtol=1e-02)
+
+
+
+if __name__ == "__main__":
+ print("Full Tests for ir intensity")
+ unittest.main()
\ No newline at end of file
diff --git a/gpu4pyscf/properties/tests/test_polarizability.py b/gpu4pyscf/properties/tests/test_polarizability.py
new file mode 100644
index 00000000..06c6ed15
--- /dev/null
+++ b/gpu4pyscf/properties/tests/test_polarizability.py
@@ -0,0 +1,128 @@
+# Copyright 2023 The GPU4PySCF Authors. All Rights Reserved.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import unittest
+import numpy as np
+import pyscf
+from pyscf import lib
+from gpu4pyscf.dft import rks, uks
+from gpu4pyscf.properties import polarizability
+
+lib.num_threads(8)
+
+atom = '''
+O 0.0000000000 -0.0000000000 0.1174000000
+H -0.7570000000 -0.0000000000 -0.4696000000
+H 0.7570000000 0.0000000000 -0.4696000000
+'''
+
+bas='def2tzvpp'
+grids_level = 6
+
+def setUpModule():
+ global mol
+ mol = pyscf.M(atom=atom, basis=bas, max_memory=32000)
+ mol.output = '/dev/null'
+ mol.build()
+ mol.verbose = 1
+
+def tearDownModule():
+ global mol
+ mol.stdout.close()
+ del mol
+
+def run_dft_polarizability(xc):
+ mf = rks.RKS(mol, xc=xc)
+ mf.grids.level = grids_level
+ e_dft = mf.kernel()
+ polar = polarizability.eval_polarizability(mf)
+ return e_dft, polar
+
+def run_dft_df_polarizability(xc):
+ mf = rks.RKS(mol, xc=xc).density_fit(auxbasis='def2-universal-jkfit')
+ mf.grids.level = grids_level
+ e_dft = mf.kernel()
+ polar = polarizability.eval_polarizability(mf)
+ return e_dft, polar
+
+
+class KnownValues(unittest.TestCase):
+ '''
+ known values are obtained by Q-Chem
+
+ $rem
+ MEM_STATIC 2000
+ JOBTYPE POLARIZABILITY
+ METHOD b3lyp
+ RESPONSE_POLAR 0
+ BASIS def2-tzvpp
+ SCF_CONVERGENCE 12
+ XC_GRID 000099000590
+ SYMMETRY FALSE
+ SYM_IGNORE = TRUE
+ $end
+
+ -----------------------------------------------------------------------------
+ Polarizability tensor [a.u.]
+ 8.5899463 -0.0000000 -0.0000000
+ -0.0000000 6.0162267 -0.0000000
+ -0.0000000 -0.0000000 7.5683123
+
+ $rem
+ MEM_STATIC 2000
+ JOBTYPE POLARIZABILITY
+ METHOD b3lyp
+ RESPONSE_POLAR -1
+ BASIS def2-tzvpp
+ SCF_CONVERGENCE 12
+ XC_GRID 000099000590
+ SYMMETRY FALSE
+ SYM_IGNORE = TRUE
+ ri_j True
+ ri_k True
+ aux_basis RIJK-def2-tzvpp
+ THRESH 14
+ $end
+
+ <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
+ Polarizability tensor [a.u.]
+ 8.5902540 -0.0000000 -0.0000000
+ 0.0000000 6.0167648 0.0000000
+ -0.0000000 -0.0000000 7.5688173
+
+ '''
+ def test_rks_b3lyp(self):
+ print('-------- RKS B3LYP -------------')
+ e_tot, polar = run_dft_polarizability('B3LYP')
+ assert np.allclose(e_tot, -76.4666494276)
+ qchem_polar = np.array([ [ 8.5899463, -0.0000000, -0.0000000],
+ [-0.0000000, 6.0162267, -0.0000000],
+ [-0.0000000, -0.0000000, 7.5683123]])
+ assert np.allclose(polar, qchem_polar)
+
+ def test_rks_b3lyp_df(self):
+ print('-------- RKS density fitting B3LYP -------------')
+ e_tot, polar = run_dft_df_polarizability('B3LYP')
+ assert np.allclose(e_tot, -76.4666819553)
+ qchem_polar = np.array([ [ 8.5902540, -0.0000000, -0.0000000],
+ [ 0.0000000, 6.0167648, -0.0000000],
+ [ -0.0000000, -0.0000000, 7.5688173]])
+ assert np.allclose(polar, qchem_polar)
+
+
+
+if __name__ == "__main__":
+ print("Full Tests for polarizabillity")
+ unittest.main()
\ No newline at end of file
diff --git a/gpu4pyscf/properties/tests/test_shielding.py b/gpu4pyscf/properties/tests/test_shielding.py
new file mode 100644
index 00000000..04352ad1
--- /dev/null
+++ b/gpu4pyscf/properties/tests/test_shielding.py
@@ -0,0 +1,140 @@
+# Copyright 2023 The GPU4PySCF Authors. All Rights Reserved.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import unittest
+import numpy as np
+import pyscf
+from pyscf import lib
+from gpu4pyscf.dft import rks, uks
+from gpu4pyscf.properties import shielding
+
+lib.num_threads(8)
+
+atom = '''
+O 0.0000000000 -0.0000000000 0.1174000000
+H -0.7570000000 -0.0000000000 -0.4696000000
+H 0.7570000000 0.0000000000 -0.4696000000
+'''
+
+bas='def2tzvpp'
+grids_level = 6
+
+def setUpModule():
+ global mol
+ mol = pyscf.M(atom=atom, basis=bas, max_memory=32000)
+ mol.output = '/dev/null'
+ mol.build()
+ mol.verbose = 1
+
+def tearDownModule():
+ global mol
+ mol.stdout.close()
+ del mol
+
+def run_dft_nmr_shielding(xc):
+ mf = rks.RKS(mol, xc=xc)
+ mf.grids.level = grids_level
+ e_dft = mf.kernel()
+ tensor = shielding.eval_shielding(mf)
+ return e_dft, tensor
+
+def run_dft_df_nmr_shielding(xc):
+ mf = rks.RKS(mol, xc=xc).density_fit(auxbasis='def2-universal-jkfit')
+ mf.grids.level = grids_level
+ e_dft = mf.kernel()
+ tensor = shielding.eval_shielding(mf)
+ return e_dft, tensor
+
+
+class KnownValues(unittest.TestCase):
+ '''
+ known values are obtained by Q-Chem
+
+ $rem
+ METHOD b3lyp
+ BASIS def2-tzvpp
+ SCF_CONVERGENCE 12
+ XC_GRID 000099000590
+ SYMMETRY FALSE
+ SYM_IGNORE TRUE
+ MOPROP 1
+ MOPROP_PERTNUM 0 ! do all perturbations at once
+ MOPROP_CONV_1ST 10 ! sets the CPSCF convergence threshold
+ MOPROP_DIIS_DIM_SS 4 ! no. of DIIS subspace vectors
+ MOPROP_MAXITER_1ST 100 ! max iterations
+ MOPROP_DIIS 5 ! turns on DIIS (=0 to turn off)
+ MOPROP_DIIS_THRESH 1
+ MOPROP_DIIS_SAVE 0
+ $end
+
+ ATOM ISOTROPIC ANISOTROPIC REL. SHIFTS
+------------------------------------------------------------------------
+ Atom O 1 332.07586444 45.66898820
+ Atom H 2 31.39150070 19.36432602
+ Atom H 3 31.39060707 19.36602790
+
+ $rem
+ METHOD b3lyp
+ BASIS def2-tzvpp
+ SCF_CONVERGENCE 10
+ XC_GRID 000099000590
+ ri_j True
+ ri_k True
+ aux_basis RIJK-def2-tzvpp
+ THRESH 14
+ SYMMETRY FALSE
+ SYM_IGNORE TRUE
+ MOPROP 1
+ MOPROP_PERTNUM 0 ! do all perturbations at once
+ MOPROP_CONV_1ST 10 ! sets the CPSCF convergence threshold
+ MOPROP_DIIS_DIM_SS 4 ! no. of DIIS subspace vectors
+ MOPROP_MAXITER_1ST 100 ! max iterations
+ MOPROP_DIIS 5 ! turns on DIIS (=0 to turn off)
+ MOPROP_DIIS_THRESH 1
+ MOPROP_DIIS_SAVE 0
+ $end
+
+
+ ATOM ISOTROPIC ANISOTROPIC REL. SHIFTS
+------------------------------------------------------------------------
+ Atom O 1 332.07961083 45.66777197
+ Atom H 2 31.39250594 19.36477246
+ Atom H 3 31.39160966 19.36647972
+
+ '''
+ def test_rks_b3lyp(self):
+ print('-------- RKS B3LYP -------------')
+ e_tot, tensor = run_dft_nmr_shielding('B3LYP')
+ nmr_total = tensor[0].get()+tensor[1].get()
+ isotropic_pyscf = np.array([nmr_total[0].trace()/3, nmr_total[1].trace()/3, nmr_total[2].trace()/3])
+ isotropic_qchem = np.array([332.07586444, 31.39150070, 31.39060707])
+
+ assert np.allclose(e_tot, -76.4666494276)
+ assert np.allclose(isotropic_pyscf, isotropic_qchem, rtol=1.0E-4)
+
+ def test_rks_b3lyp_df(self):
+ print('-------- RKS density fitting B3LYP -------------')
+ e_tot, tensor = run_dft_df_nmr_shielding('B3LYP')
+ nmr_total = tensor[0].get()+tensor[1].get()
+ isotropic_pyscf = np.array([nmr_total[0].trace()/3, nmr_total[1].trace()/3, nmr_total[2].trace()/3])
+ isotropic_qchem = np.array([332.07961083, 31.39250594, 31.39160966])
+ assert np.allclose(e_tot, -76.4666819553)
+ assert np.allclose(isotropic_pyscf, isotropic_qchem, rtol=1.0E-4)
+
+
+
+if __name__ == "__main__":
+ print("Full Tests for nmr shielding constants")
+ unittest.main()
\ No newline at end of file