import numpy as np
from pyscf import lib
from pyscf.lib import logger
from pyscf.fci import direct_spin1
from pyscf.mcscf import newton_casscf
from pyscf.grad import casscf as casscf_grad
from pyscf.grad import sacasscf as sacasscf_grad
from functools import reduce
# The extension from gradients -> NACs has three basic steps:
# 0. ("state" index integer -> tuple)
# 1. fcisolver.make_rdm12 -> fcisolver.trans_rdm12
# 2. remove core-orbital and nuclear contributions to everything
# 3. option to include the "csf contribution"
# Additional good ideas:
# a. Option to multiply NACs by the energy difference to control
# singularities
def _unpack_state(state):
assert len(state) == 2, "derivative couplings are defined between 2 states"
return state[0], state[1]
[docs]
def grad_elec_core(mc_grad, mo_coeff=None, atmlst=None, eris=None, mf_grad=None):
"""Compute the core-electron part of the CASSCF (Hellmann-Feynman)
gradient using a modified RHF grad_elec call."""
mc = mc_grad.base
if mo_coeff is None: mo_coeff = mc.mo_coeff
if eris is None: eris = mc.ao2mo (mo_coeff)
if mf_grad is None: mf_grad = mc._scf.nuc_grad_method ()
ncore = mc.ncore
moH = mo_coeff.conj ().T
f0 = (moH @ mc.get_hcore () @ mo_coeff) + eris.vhf_c
mo_energy = f0.diagonal ().copy ()
mo_occ = np.zeros_like (mo_energy)
mo_occ[:ncore] = 2.0
f0 *= mo_occ[None,:]
dme0 = lambda * args: mo_coeff @ ((f0+f0.T)*.5) @ moH
with lib.temporary_env (mf_grad, make_rdm1e=dme0, verbose=0):
with lib.temporary_env (mf_grad.base, mo_coeff=mo_coeff, mo_occ=mo_occ):
# Second level there should become unnecessary in future, if anyone
# ever gets around to cleaning up pyscf.df.grad.rhf & pyscf.grad.rhf
de = mf_grad.grad_elec (mo_coeff=mo_coeff, mo_energy=mo_energy,
mo_occ=mo_occ, atmlst=atmlst)
return de
[docs]
def grad_elec_active (mc_grad, mo_coeff=None, ci=None, atmlst=None,
eris=None, mf_grad=None, verbose=None):
'''Compute the active-electron part of the CASSCF (Hellmann-Feynman)
gradient by subtracting the core-electron part.'''
t0 = (logger.process_clock (), logger.perf_counter ())
mc = mc_grad.base
log = logger.new_logger (mc_grad, verbose)
if mf_grad is None: mf_grad=mc._scf.nuc_grad_method ()
de = mc_grad.grad_elec (mo_coeff=mo_coeff, ci=ci, atmlst=atmlst,
verbose=0)
de -= grad_elec_core (mc_grad, mo_coeff=mo_coeff, atmlst=atmlst,
eris=eris, mf_grad=mf_grad)
log.debug ('CASSCF active-orbital gradient:\n{}'.format (de))
log.timer ('CASSCF active-orbital gradient', *t0)
return de
[docs]
def gen_g_hop_active (mc, mo, ci0, eris, verbose=None):
'''Compute the active-electron part of the orbital rotation gradient
by patching out the appropriate block of eris.vhf_c'''
moH = mo.conj ().T
ncore = mc.ncore
vnocore = eris.vhf_c.copy ()
vnocore[:,:ncore] = -moH @ mc.get_hcore () @ mo[:,:ncore]
with lib.temporary_env (eris, vhf_c=vnocore):
return newton_casscf.gen_g_hop (mc, mo, ci0, eris, verbose=verbose)
def _nac_csf (mol, mf_grad, tm1, atmlst):
if atmlst is None: atmlst = list (range (mol.natm))
aoslices = mol.aoslice_by_atom ()
s1 = mf_grad.get_ovlp (mol)
# if libcint documentation is to be trusted, mf_grad.get_ovlp
# corresponds to differentiating on the SECOND index: <p|dq/dR>
nac = np.zeros ((len(atmlst), 3))
for k, ia in enumerate (atmlst):
shl0, shl1, p0, p1 = aoslices[ia]
nac[k] += 0.5*np.einsum ('xij,ij->x', s1[:,p0:p1], tm1[p0:p1])
return nac
[docs]
def nac_csf (mc_grad, mo_coeff=None, ci=None, state=None, mf_grad=None,
atmlst=None):
'''Compute the "CSF contribution" to the SA-CASSCF NAC'''
mc = mc_grad.base
if mo_coeff is None: mo_coeff = mc.mo_coeff
if ci is None: ci = mc.ci
if state is None: state = mc_grad.state
if mf_grad is None: mf_grad = mc._scf.nuc_grad_method ()
if atmlst is None: atmlst = mc_grad.atmlst
mol = mc.mol
ket, bra = _unpack_state (state)
ncore, ncas, nelecas = mc.ncore, mc.ncas, mc.nelecas
castm1 = direct_spin1.trans_rdm1 (ci[bra], ci[ket], ncas, nelecas)
# if PySCF commentary is to be trusted, trans_rdm1[p,q] is
# <bra|q'p|ket>. I want <bra|p'q - q'p|ket>.
castm1 = castm1.conj ().T - castm1
mo_cas = mo_coeff[:,ncore:][:,:ncas]
tm1 = reduce (np.dot, (mo_cas, castm1, mo_cas.conj ().T))
return _nac_csf (mol, mf_grad, tm1, atmlst)
[docs]
class NonAdiabaticCouplings (sacasscf_grad.Gradients):
'''SA-CASSCF non-adiabatic couplings (NACs) between states
kwargs/attributes:
state : tuple of length 2
The NACs returned are <state[1]|d(state[0])/dR>.
In other words, state = (ket, bra).
mult_ediff : logical
If True, returns NACs multiplied by the energy difference.
Useful near conical intersections to avoid numerical problems.
use_etfs : logical
If True, use the ``electron translation factors'' of Fatehi and
Subotnik [JPCL 3, 2039 (2012)], which guarantee conservation of
total electron + nuclear momentum when the nuclei are moving
(i.e., in non-adiabatic molecular dynamics). This corresponds
to omitting the so-called ``CSF contribution'' [cf. JCTC 12,
3636 (2016)].
'''
def __init__(self, mc, state=None, mult_ediff=False, use_etfs=False):
self.mult_ediff = mult_ediff
self.use_etfs = use_etfs
if state is not None:
assert len(state) == 2, "derivative couplings are defined between 2 states"
sacasscf_grad.Gradients.__init__(self, mc, state=state)
[docs]
def make_fcasscf_nacs (self, state=None, casscf_attr=None,
fcisolver_attr=None):
if state is None: state = self.state
if casscf_attr is None: casscf_attr = {}
if fcisolver_attr is None: fcisolver_attr = {}
ket, bra = _unpack_state (state)
ci, ncas, nelecas = self.base.ci, self.base.ncas, self.base.nelecas
# TODO: use fcisolver.fcisolvers in state-average mix case for this
castm1, castm2 = direct_spin1.trans_rdm12 (ci[bra], ci[ket], ncas,
nelecas)
castm1 = 0.5 * (castm1 + castm1.T)
castm2 = 0.5 * (castm2 + castm2.transpose (1,0,3,2))
fcisolver_attr['make_rdm12'] = lambda *args, **kwargs : (castm1, castm2)
fcisolver_attr['make_rdm1'] = lambda *args, **kwargs : castm1
fcisolver_attr['make_rdm2'] = lambda *args, **kwargs : castm2
return sacasscf_grad.Gradients.make_fcasscf (self,
state=ket, casscf_attr=casscf_attr, fcisolver_attr=fcisolver_attr)
[docs]
def get_wfn_response (self, atmlst=None, state=None, verbose=None, mo=None, ci=None, **kwargs):
if state is None: state = self.state
if atmlst is None: atmlst = self.atmlst
if verbose is None: verbose = self.verbose
if mo is None: mo = self.base.mo_coeff
if ci is None: ci = self.base.ci
log = logger.new_logger (self, verbose)
ket, bra = _unpack_state (state)
fcasscf = self.make_fcasscf_nacs (state)
fcasscf.mo_coeff = mo
fcasscf.ci = ci[ket]
eris = fcasscf.ao2mo (mo)
g_all_ket = gen_g_hop_active (fcasscf, mo, ci[ket], eris, verbose)[0]
g_all = np.zeros (self.nlag)
g_all[:self.ngorb] = g_all_ket[:self.ngorb]
# The fun thing about the ci sector is that you swap them (&/2):
# <I|[H,|A><I|-|I><A|]|J> = <A|H|J> = <J|[H,|A><J|-|J><A|]|J>/2
# (It should be zero for converged SA-CASSCF anyway, though)
g_ci_bra = 0.5 * g_all_ket[self.ngorb:]
g_all_bra = gen_g_hop_active (fcasscf, mo, ci[bra], eris, verbose)[0]
g_ci_ket = 0.5 * g_all_bra[self.ngorb:]
# I have to make sure they don't talk to each other because the
# preconditioner doesn't explore that space at all. Should I
# instead solve at the init_guess step, like in MC-PDFT?
# In practice it should all be zeros but how tightly does
# everything have to be converged?
ndet_ket = (self.na_states[ket], self.nb_states[ket])
ndet_bra = (self.na_states[bra], self.nb_states[bra])
if ndet_ket==ndet_bra:
ket2bra = np.dot (ci[bra].conj ().ravel (), g_ci_ket)
bra2ket = np.dot (ci[ket].conj ().ravel (), g_ci_bra)
log.debug ('SA-CASSCF <bra|H|ket>,<ket|H|bra> check: %5.3g , %5.3g',
ket2bra, bra2ket)
g_ci_ket -= ket2bra * ci[bra].ravel ()
g_ci_bra -= bra2ket * ci[ket].ravel ()
ndet_ket = ndet_ket[0]*ndet_ket[1]
ndet_bra = ndet_bra[0]*ndet_bra[1]
# No need to reshape or anything, just use the magic of repeated slicing
offs_ket = (sum ([na * nb for na, nb in zip(
self.na_states[:ket], self.nb_states[:ket])])
if ket > 0 else 0)
offs_bra = (sum ([na * nb for na, nb in zip(
self.na_states[:bra], self.nb_states[:bra])])
if ket > 0 else 0)
g_all[self.ngorb:][offs_ket:][:ndet_ket] = g_ci_ket
g_all[self.ngorb:][offs_bra:][:ndet_bra] = g_ci_bra
return g_all
[docs]
def get_ham_response (self, state=None, atmlst=None, verbose=None, mo=None,
ci=None, eris=None, mf_grad=None, **kwargs):
if state is None: state = self.state
if atmlst is None: atmlst = self.atmlst
if verbose is None: verbose = self.verbose
if mo is None: mo = self.base.mo_coeff
if ci is None: ci = self.base.ci
if mf_grad is None: mf_grad = self.base._scf.nuc_grad_method ()
if eris is None and self.eris is None:
eris = self.eris = self.base.ao2mo (mo)
elif eris is None:
eris = self.eris
use_etfs = kwargs.get ('use_etfs', self.use_etfs)
ket, bra = _unpack_state (state)
fcasscf_grad = casscf_grad.Gradients (self.make_fcasscf_nacs (state))
nac = grad_elec_active (fcasscf_grad, mo_coeff=mo, ci=ci[ket],
eris=eris, atmlst=atmlst, verbose=verbose)
if not use_etfs: nac += self.nac_csf (
mo_coeff=mo, ci=ci, state=state, mf_grad=mf_grad, atmlst=atmlst)
return nac
[docs]
def nac_csf (self, mo_coeff=None, ci=None, state=None, mf_grad=None, atmlst=None):
if state is None: state = self.state
if atmlst is None: atmlst = self.atmlst
if mo_coeff is None: mo_coeff = self.base.mo_coeff
if ci is None: ci = self.base.ci
if mf_grad is None: mf_grad = self.base._scf.nuc_grad_method ()
nac = nac_csf (self, mo_coeff=mo_coeff, ci=ci, state=state,
mf_grad=mf_grad, atmlst=atmlst)
ket, bra = _unpack_state (state)
e_bra = self.base.e_states[bra]
e_ket = self.base.e_states[ket]
nac *= e_bra - e_ket
return nac
[docs]
def kernel (self, *args, **kwargs):
mult_ediff = kwargs.get ('mult_ediff', self.mult_ediff)
state = kwargs.get ('state', self.state)
assert len(state) == 2, "derivative couplings are defined between 2 states"
if state[0] == state[1]:
mol = kwargs.get('mol', self.mol)
atmlst = kwargs.get('atmlst', range(mol.natm))
return np.zeros((len(atmlst), 3))
nac = sacasscf_grad.Gradients.kernel (self, *args, **kwargs)
if not mult_ediff:
ket, bra = _unpack_state (state)
e_bra = self.base.e_states[bra]
e_ket = self.base.e_states[ket]
nac /= e_bra - e_ket
return nac
if __name__=='__main__':
from pyscf import gto, scf, mcscf
from scipy import linalg
mol = gto.M (atom = 'Li 0 0 0; H 0 0 1.5', basis='sto-3g',
output='sacasscf_nacs.log', verbose=lib.logger.INFO)
mf = scf.RHF (mol).run ()
mc = mcscf.CASSCF (mf, 2, 2).fix_spin_(ss=0).state_average ([0.5,0.5]).run (conv_tol=1e-10)
openmolcas_energies = np.array ([-7.85629118, -7.72175252])
print ("energies:",mc.e_states)
print ("disagreement w openmolcas:", np.around (mc.e_states-openmolcas_energies, 8))
mc_nacs = NonAdiabaticCouplings (mc)
print ("no csf contr")
nac_01 = mc_nacs.kernel (state=(0,1), use_etfs=True)
nac_10 = mc_nacs.kernel (state=(1,0), use_etfs=True)
nac_01_mult = mc_nacs.kernel (state=(0,1), use_etfs=True, mult_ediff=True)
nac_10_mult = mc_nacs.kernel (state=(1,0), use_etfs=True, mult_ediff=True)
print ("antisym")
print (nac_01)
print ("checking antisym:",linalg.norm(nac_01+nac_10))
print ("sym")
print (nac_01_mult)
print ("checking sym:",linalg.norm(nac_01_mult-nac_10_mult))
print ("incl csf contr")
nac_01 = mc_nacs.kernel (state=(0,1), use_etfs=False)
nac_10 = mc_nacs.kernel (state=(1,0), use_etfs=False)
nac_01_mult = mc_nacs.kernel (state=(0,1), use_etfs=False, mult_ediff=True)
nac_10_mult = mc_nacs.kernel (state=(1,0), use_etfs=False, mult_ediff=True)
print ("antisym")
print (nac_01)
print ("checking antisym:",linalg.norm(nac_01+nac_10))
print ("sym")
print (nac_01_mult)
print ("checking sym:",linalg.norm(nac_01_mult-nac_10_mult))
print ("Check gradients")
mc_grad = mc.nuc_grad_method ()
de_0 = mc_grad.kernel (state=0)
print (de_0)
de_1 = mc_grad.kernel (state=1)
print (de_1)