#!/usr/bin/env python
# Copyright 2014-2024 The PySCF Developers. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import numpy as np
from pyscf import mcpdft
from pyscf.grad import mspdft as mspdft_grad
from pyscf import lib
from pyscf.fci import direct_spin1
from pyscf.nac import sacasscf as sacasscf_nacs
from functools import reduce
_unpack_state = mspdft_grad._unpack_state
_nac_csf = sacasscf_nacs._nac_csf
[docs]
def nac_model (mc_grad, mo_coeff=None, ci=None, si_bra=None, si_ket=None,
mf_grad=None, atmlst=None):
'''Compute the "model-state contribution" to the MS-PDFT NAC'''
mc = mc_grad.base
mol = mc.mol
ci_bra = np.tensordot (si_bra, ci, axes=1)
ci_ket = np.tensordot (si_ket, ci, axes=1)
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 (mspdft_grad.Gradients):
'''MS-PDFT 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 ``model state contribution''.
'''
def __init__(self, mc, state=None, mult_ediff=False, use_etfs=False):
self.mult_ediff = mult_ediff
self.use_etfs = use_etfs
self.state = state
mspdft_grad.Gradients.__init__(self, mc)
[docs]
def get_wfn_response (self, si_bra=None, si_ket=None, state=None, si=None,
verbose=None, **kwargs):
g_all = mspdft_grad.Gradients.get_wfn_response (
self, si_bra=si_bra, si_ket=si_ket, state=state, si=si,
verbose=verbose, **kwargs
)
g_orb, g_ci, g_is = self.unpack_uniq_var (g_all)
if state is None: state = self.state
ket, bra = _unpack_state (state)
if si is None: si = self.base.si
if si_bra is None: si_bra = si[:,bra]
if si_ket is None: si_ket = si[:,ket]
nroots = self.nroots
log = lib.logger.new_logger (self, verbose)
g_model = np.multiply.outer (si_bra.conj (), si_ket)
g_model -= g_model.T
g_model *= self.base.e_states[bra]-self.base.e_states[ket]
g_model = g_model[np.tril_indices (nroots, k=-1)]
log.debug ("NACs g_is additional component:\n{}".format (g_model))
return self.pack_uniq_var (g_orb, g_ci, g_is+g_model)
[docs]
def get_ham_response (self, **kwargs):
nac = mspdft_grad.Gradients.get_ham_response (self, **kwargs)
use_etfs = kwargs.get ('use_etfs', self.use_etfs)
if not use_etfs:
verbose = kwargs.get ('verbose', self.verbose)
log = lib.logger.new_logger (self, verbose)
nac_model = self.nac_model (**kwargs)
log.info ('NACs model-state contribution:\n{}'.format (nac_model))
nac += nac_model
return nac
[docs]
def nac_model (self, mo_coeff=None, ci=None, si=None, si_bra=None,
si_ket=None, state=None, mf_grad=None, atmlst=None,
**kwargs):
if state is None: state = self.state
ket, bra = _unpack_state (state)
if mo_coeff is None: mo_coeff = self.base.mo_coeff
if ci is None: ci = self.base.ci
if si is None: si = self.base.si
if si_bra is None: si_bra = si[:,bra]
if si_ket is None: si_ket = si[:,ket]
if mf_grad is None: mf_grad = self.base.get_rhf_base ().nuc_grad_method ()
if atmlst is None: atmlst = self.atmlst
nac = nac_model (self, mo_coeff=mo_coeff, ci=ci, si_bra=si_bra,
si_ket=si_ket, mf_grad=mf_grad, atmlst=atmlst)
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)
nac = mspdft_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
from mrh.my_pyscf.dft.openmolcas_grids import quasi_ultrafine
from scipy import linalg
mol = gto.M (atom = 'Li 0 0 0; H 0 0 1.5', basis='sto-3g',
output='mspdft_nacs.log', verbose=lib.logger.INFO)
mf = scf.RHF (mol).run ()
mc = mcpdft.CASSCF (mf, 'ftLDA,VWN3', 2, 2, grids_attr=quasi_ultrafine)
mc = mc.fix_spin_(ss=0).multi_state ([0.5,0.5], 'cms').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")
print ("antisym")
nac_01 = mc_nacs.kernel (state=(0,1), use_etfs=True)
nac_10 = mc_nacs.kernel (state=(1,0), use_etfs=True)
print (nac_01)
print (nac_10)
print ("checking antisym:",linalg.norm(nac_01+nac_10))
print ("sym")
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 (nac_01_mult)
print (nac_10_mult)
print ("checking sym:",linalg.norm(nac_01_mult-nac_10_mult))
print ("incl csf contr")
print ("antisym")
nac_01 = mc_nacs.kernel (state=(0,1), use_etfs=False)
nac_10 = mc_nacs.kernel (state=(1,0), use_etfs=False)
print (nac_01)
print ("checking antisym:",linalg.norm(nac_01+nac_10))
print ("sym")
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 (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)
#from mrh.my_pyscf.tools.molcas2pyscf import *
#mol = get_mol_from_h5 ('LiH_sa2casscf22_sto3g.rasscf.h5',
# output='sacasscf_nacs_fromh5.log',
# verbose=lib.logger.INFO)
#mo = get_mo_from_h5 (mol, 'LiH_sa2casscf22_sto3g.rasscf.h5')
#nac_etfs_ref = np.array ([9.14840490109073E-02, -9.14840490109074E-02])
#nac_ref = np.array ([1.83701578323929E-01, -6.91459741744125E-02])
#mf = scf.RHF (mol).run ()
#mc = mcscf.CASSCF (mol, 2, 2).fix_spin_(ss=0).state_average ([0.5,0.5])
#mc.run (mo, natorb=True, conv_tol=1e-10)
#mc_nacs = NonAdiabaticCouplings (mc)
#nac = mc_nacs.kernel (state=(0,1))
#print (nac)
#print (nac_ref)
#nac_etfs = mc_nacs.kernel (state=(0,1), use_etfs=True)
#print (nac_etfs)
#print (nac_etfs_ref)