Skip to content

Commit

Permalink
CMS-PDFT Nonadiabatic Coupling (#44)
Browse files Browse the repository at this point in the history
* update and add cmspdft nacs

* add example

* update doc

* fix flake8

* update doc

* add cms derivative coupling doi
  • Loading branch information
matthew-hennefarth authored Feb 19, 2024
1 parent 1077e48 commit 5351178
Show file tree
Hide file tree
Showing 6 changed files with 541 additions and 1 deletion.
3 changes: 3 additions & 0 deletions doc/mcpdft/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ Features
3. CMS-PDFT
- Transition electric dipole moment (non-hybrid functionals only) for:
1. CMS-PDFT
- Derivative couplings for:
1. CMS-PDFT [*JPC A* **2024**]
* Multi-configuration density-coherence functional theory (MC-DCFT)
total energy: [*JCTC* **2021**, *17*, 2775]

Expand All @@ -81,6 +83,7 @@ Features
[*Mol Phys* **2022**, 120]: http://dx.doi.org/10.1080/00268976.2022.2110534
[*Faraday Discuss* **2020**, 224, 348-372]: http://dx.doi.org/10.1039/D0FD00037J
[*JCTC* **2023**, *19*, 3172]: https://dx.doi.org/10.1021/acs.jctc.3c00207
[*JPC A* **2024**]: https://dx.doi.org/10.1021/acs.jpca.3c07048

[comment]: <> (Code hyperlinks)
[examples/mcpdft/02-hybrid_functionals.py]: examples/mcpdft/02-hybrid_functionals.py
Expand Down
116 changes: 116 additions & 0 deletions examples/nac/01-cmspdft_nac.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
from pyscf import gto, scf, lib, mcpdft

# NAC signs are really, really hard to nail down.
# There are arbitrary signs associated with
# 1. The MO coefficients
# 2. The CI vectors
# 3. Almost any kind of post-processing (natural-orbital analysis, etc.)
# 4. Developer convention on whether the bra index or ket index is 1st
# It MIGHT help comparison to OpenMolcas if you load a rasscf.h5 file
# I TRIED to choose the same convention for #4 as OpenMolcas.
mol = gto.M (atom='Li 0 0 0;H 1.5 0 0', basis='sto-3g',
output='LiH_cms2ftlda22_sto3g.log', verbose=lib.logger.INFO)

mf = scf.RHF (mol).run ()
mc = mcpdft.CASSCF (mf, 'ftLDA,VWN3', 2, 2, grids_level=3)
# Quasi-ultrafine is ALMOST the same thing as
# ```
# grid input
# nr=100
# lmax=41
# rquad=ta
# nopr
# noro
# end of grid input
# ```
# in SEWARD
mc.fix_spin_(ss=0, shift=1)
mc = mc.multi_state ([0.5,0.5], 'cms').run (conv_tol=1e-10)

mc_nacs = mc.nac_method()

# 1. <1|d0/dR>
# Equivalent OpenMolcas input:
# ```
# &ALASKA
# NAC=1 2
# ```
nac = mc_nacs.kernel (state=(0,1))
print ("\nNAC <1|d0/dR>:\n",nac)
print ("Notice that according to the NACs printed above, rigidly moving the")
print ("molecule along the bond axis changes the electronic wave function, which")
print ("is obviously unphysical. This broken translational symmetry is due to the")
print ("antisymmetric orbital-overlap derivative in the Hellmann-Feynman part of")
print ("the 'model state contribution'. Omitting the antisymmetric orbital-overlap")
print ("derivative corresponds to the use of the 'electron-translation factors' of")
print ("Fatehi and Subotnik and is requested by passing 'use_etfs=True'.")

# 2. <1|d0/dR> w/ ETFs (i.e., w/out model-state Hellmann-Feynman contribution)
# Equivalent OpenMolcas input:
# ```
# &ALASKA
# NAC=1 2
# NOCSF
# ```
nac = mc_nacs.kernel (state=(0,1), use_etfs=True)
print ("\nNAC <1|d0/dR> w/ ETFs:\n", nac)
print ("These NACs are much more well-behaved: moving the molecule rigidly around")
print ("in space doesn't induce any change to the electronic wave function.")

# 3. <0|d1/dR>
# Equivalent OpenMolcas input:
# ```
# &ALASKA
# NAC=2 1
# ```
nac = mc_nacs.kernel (state=(1,0))
print ("\nThe NACs are antisymmetric with respect to state transposition.")
print ("NAC <0|d1/dR>:\n", nac)

# 4. <0|d1/dR> w/ ETFs
# Equivalent OpenMolcas input:
# ```
# &ALASKA
# NAC=2 1
# NOCSF
# ```
nac = mc_nacs.kernel (state=(1,0), use_etfs=True)
print ("NAC <0|d1/dR> w/ ETFs:\n", nac)


# 5. <1|d0/dR>*(E1-E0) = <0|d1/dR>*(E0-E1)
# I'm not aware of any OpenMolcas equivalent for this, but all the information
# should obviously be in the output file, as long as you aren't right at a CI.
nac_01 = mc_nacs.kernel (state=(0,1), mult_ediff=True)
nac_10 = mc_nacs.kernel (state=(1,0), mult_ediff=True)
print ("\nNACs diverge at conical intersections (CI). The important question")
print ("is how quickly it diverges. You can get at this by calculating NACs")
print ("multiplied by the energy difference using the keyword 'mult_ediff=True'.")
print ("This yields a quantity which is symmetric wrt state interchange and is")
print ("finite at a CI.")
print ("NAC <1|d0/dR>*(E1-E0):\n", nac_01)
print ("NAC <0|d1/dR>*(E0-E1):\n", nac_10)

# 6. <1|d0/dR>*(E1-E0) w/ ETFs
# For comparison with 7 below.
nac_01 = mc_nacs.kernel (state=(0,1), mult_ediff=True, use_etfs=True)
nac_10 = mc_nacs.kernel (state=(1,0), mult_ediff=True, use_etfs=True)
print ("\nUnlike the SA-CASSCF case, using both 'use_etfs=True' and")
print ("'mult_ediff=True' DOES NOT reproduce the first derivative of the")
print ("off-diagonal element of the potential matrix. This is because the")
print ("model-state contribution generates a symmetric contribution to the")
print ("response equations and changes the values of the Lagrange multipliers,")
print ("even if the Hellmann-Feynman part of the model-state contribution is")
print ("omitted. You can get the gradients of the potential couplings by")
print ("passing a tuple to the gradient method instance instead.")
print ("<1|d0/dR>*(E1-E0) w/ ETFs:\n",nac_01)
print ("<0|d1/dR>*(E0-E1) w/ ETFs:\n",nac_10)

# 7. <1|dH/dR|0>
# THIS is the quantity one uses to optimize MECIs
mc_grad = mc.nuc_grad_method ()
v01 = mc_grad.kernel (state=(0,1))
v10 = mc_grad.kernel (state=(1,0))
print ("<1|dH/dR|0>:\n", v01)
print ("<0|dH/dR|1>:\n", v10)

5 changes: 4 additions & 1 deletion pyscf/mcpdft/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,7 @@ class MultiStateMCPDFTSolver :
from pyscf.df import grad as df_grad
df_grad.__path__.append (mydfgradpath)
df_grad.__path__=list(set(df_grad.__path__))

mynacpath = os.path.join(mypath, "nac")
from pyscf import nac
nac.__path__.append(mynacpath)
nac.__path__ = list(set(nac.__path__))
12 changes: 12 additions & 0 deletions pyscf/mcpdft/mspdft.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,18 @@ def nuc_grad_method (self):
from pyscf.grad.mspdft import Gradients
return Gradients (self)

def nac_method(self):
if not isinstance(self, mc1step.CASSCF):
raise NotImplementedError("CASCI-based PDFT NACs")
elif getattr(self, 'frozen', None) is not None:
raise NotImplementedError("PDFT NACs with frozen orbitals")
elif isinstance(self, _DFCASSCF):
raise NotImplementedError("PDFT NACs with density fitting")
else:
from pyscf.nac.mspdft import NonAdiabaticCouplings

return NonAdiabaticCouplings(self)

def dip_moment (self, unit='Debye', origin='Coord_Center', state=None):
if not isinstance (self, mc1step.CASSCF):
raise NotImplementedError ("CASCI-based PDFT dipole moments")
Expand Down
196 changes: 196 additions & 0 deletions pyscf/nac/mspdft.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
#!/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

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)

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)

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)

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

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._scf.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

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)


Loading

0 comments on commit 5351178

Please sign in to comment.