#!/usr/bin/env python
# Copyright 2014-2022 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 math
import numpy as np
from scipy import linalg
from pyscf import mcpdft
from pyscf.grad import mcpdft as mcpdft_grad
from pyscf import lib
from pyscf.lib import logger
from pyscf.fci import direct_spin1
from pyscf.mcscf import mc1step, newton_casscf
from pyscf.grad import rhf as rhf_grad
from pyscf.grad import casscf as casscf_grad
from pyscf.grad import sacasscf as sacasscf_grad
from pyscf import __config__
from itertools import product
# PySCF-Forge installation check
try:
from pyscf.csf_fci.csf import CSFFCISolver
except ModuleNotFoundError:
[docs]
class CSFFCISolver:
pass
CONV_TOL_DIABATIZE = getattr(__config__, 'mcpdft_mspdft_conv_tol_diabatize', 1e-8)
SING_TOL_DIABATIZE = getattr(__config__, 'mcpdft_mspdft_sing_tol_diabatize', 1e-8)
SING_STEP_TOL = getattr(__config__, 'grad_mspdft_sing_step_tol', 2*math.pi)
def _unpack_state (state):
if hasattr (state, '__len__'): return state[0], state[1]
return state, state
# TODO: state-average-mix generalization ?
[docs]
def make_rdm12_heff_offdiag (mc, ci, si_bra, si_ket):
'''Compute <bra|O|ket> - sum_i <i|O|i>, where O is the 1- and 2-RDM
operator product, and |bra> and |ket> are both states spanning the
vector space of |i>, which are multi-determinantal many-electron
states in an active space.
Args:
mc : object of class CASCI or CASSCF
Only "ncas" and "nelecas" are used, to determine Hilbert
of ci
ci : ndarray or list of length (nroots)
Contains CI vectors spanning a model space
si_bra : ndarray of shape (nroots)
Coefficients of ci elements for state |bra>
si_ket : ndarray of shape (nroots)
Coefficients of ci elements for state |ket>
Returns:
casdm1 : ndarray of shape [ncas,]*2
Contains O = p'q case
casdm2 : ndarray of shape [ncas,]*4
Contains O = p'q'sr case
'''
ncas, nelecas = mc.ncas, mc.nelecas
nroots = len (ci)
ci_arr = np.asarray (ci)
ci_bra = np.tensordot (si_bra, ci_arr, axes=1)
ci_ket = np.tensordot (si_ket, ci_arr, axes=1)
casdm1, casdm2 = direct_spin1.trans_rdm12 (ci_bra, ci_ket, ncas, nelecas)
ddm1 = np.zeros ((nroots, ncas, ncas), dtype=casdm1.dtype)
ddm2 = np.zeros ((nroots, ncas, ncas, ncas, ncas), dtype=casdm1.dtype)
for i in range (nroots):
ddm1[i,...], ddm2[i,...] = direct_spin1.make_rdm12 (ci[i], ncas,
nelecas)
si_diag = si_bra * si_ket
casdm1 -= np.tensordot (si_diag, ddm1, axes=1)
casdm2 -= np.tensordot (si_diag, ddm2, axes=1)
return casdm1, casdm2
# TODO: docstring?
[docs]
def mspdft_heff_response (mc_grad, mo=None, ci=None,
si_bra=None, si_ket=None, state=None,
heff_mcscf=None, eris=None):
'''Compute the orbital and intermediate-state rotation response
vector in the context of an MS-PDFT gradient calculation '''
mc = mc_grad.base
if mo is None: mo = mc_grad.mo_coeff
if ci is None: ci = mc_grad.ci
if state is None: state = mc_grad.state
ket, bra = _unpack_state (state)
if si_bra is None: si_bra = mc.si[:,bra]
if si_ket is None: si_ket = mc.si[:,ket]
if heff_mcscf is None: heff_mcscf = mc.heff_mcscf
if eris is None: eris = mc.ao2mo (mo)
nroots, ncore = mc_grad.nroots, mc.ncore
moH = mo.conj ().T
# Orbital rotation (no all-core DM terms allowed!)
# (Factor of 2 is convention difference between mc1step and newton_casscf)
casdm1, casdm2 = make_rdm12_heff_offdiag (mc, ci, si_bra, si_ket)
casdm1 = 0.5 * (casdm1 + casdm1.T)
casdm2 = 0.5 * (casdm2 + casdm2.transpose (1,0,3,2))
vnocore = eris.vhf_c.copy ()
vnocore[:,:ncore] = -moH @ mc.get_hcore () @ mo[:,:ncore]
with lib.temporary_env (eris, vhf_c=vnocore):
g_orb = 2 * mc1step.gen_g_hop (mc, mo, 1, casdm1, casdm2, eris)[0]
g_orb = mc.unpack_uniq_var (g_orb)
# Intermediate state rotation (TODO: state-average-mix generalization)
braH = np.dot (si_bra, heff_mcscf)
Hket = np.dot (heff_mcscf, si_ket)
si2 = si_bra * si_ket
g_is = np.multiply.outer (si_ket, braH)
g_is += np.multiply.outer (si_bra, Hket)
g_is -= 2 * si2[:,None] * heff_mcscf
g_is -= g_is.T
g_is = g_is[np.tril_indices (nroots, k=-1)]
return g_orb, g_is
# TODO: docstring?
[docs]
def mspdft_heff_HellmanFeynman (mc_grad, atmlst=None, mo=None, ci=None,
si=None, si_bra=None, si_ket=None, state=None, eris=None, mf_grad=None,
verbose=None, **kwargs):
mc = mc_grad.base
if atmlst is None: atmlst = mc_grad.atmlst
if mo is None: mo = mc.mo_coeff
if ci is None: ci = mc.ci
if si is None: si = getattr (mc, 'si', None)
if state is None: state = mc_grad.state
ket, bra = _unpack_state (state)
if si_bra is None: si_bra = si[:,bra]
if si_ket is None: si_ket = si[:,ket]
if eris is None: eris = mc.ao2mo (mo)
if mf_grad is None: mf_grad = mc.get_rhf_base ().nuc_grad_method ()
if verbose is None: verbose = mc_grad.verbose
ncore = mc.ncore
log = logger.new_logger (mc_grad, verbose)
ci0 = np.zeros_like (ci[0])
# CASSCF grad with effective RDMs
t0 = (logger.process_clock (), logger.perf_counter ())
casdm1, casdm2 = make_rdm12_heff_offdiag (mc, ci, si_bra, si_ket)
casdm1 = 0.5 * (casdm1 + casdm1.T)
casdm2 = 0.5 * (casdm2 + casdm2.transpose (1,0,3,2))
dm12 = lambda * args: (casdm1, casdm2)
fcasscf = mc_grad.make_fcasscf (state=ket,
fcisolver_attr={'make_rdm12' : dm12})
# TODO: DFeri functionality
# Perhaps by patching fcasscf.nuc_grad_method?
fcasscf_grad = fcasscf.nuc_grad_method ()
#fcasscf_grad = casscf_grad.Gradients (fcasscf)
de = fcasscf_grad.kernel (mo_coeff=mo, ci=ci0, atmlst=atmlst, verbose=0)
# subtract nuc-nuc and core-core (patching out simplified gfock terms)
moH = mo.conj ().T
f0 = (moH @ mc.get_hcore () @ mo) + 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 @ ((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, 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
dde = mf_grad.kernel (mo_coeff=mo, mo_energy=mo_energy, mo_occ=mo_occ,
atmlst=atmlst)
de -= dde
log.debug ('MS-PDFT gradient off-diagonal H-F terms:\n{}'.format (de))
log.timer ('MS-PDFT gradient off-diagonal H-F terms', *t0)
return de
[docs]
def get_diabfns (obj):
'''Interpret the name of the MS-PDFT method as a pair of functions
which compute the derivatives of a particular objective function
with respect to wave function parameters and geometry perturbations,
excluding first and second derivatives wrt intermediate state
rotations, which is handled by the energy-class version of this
function.
Args:
obj : string
Specify particular MS-PDFT method. Currently, only "CMS" is
supported. Not case-sensitive.
Returns:
diab_response : callable
Computes the orbital-rotation and CI-transfer sectors of the
Hessian-vector product of the MS objective function for a
vector of intermediate-state rotations
diab_grad : callable
Computes the gradient of the MS objective function wrt
geometry perturbation
'''
if obj.upper () == 'CMS':
from pyscf.grad.cmspdft import diab_response, diab_grad
else:
raise RuntimeError ('MS-PDFT type not supported')
return diab_response, diab_grad
# TODO: docstring? especially considering the "si_bra," "si_ket"
# functionality??
# TODO: figure out how to log the gradients with the right method name!
[docs]
class Gradients (mcpdft_grad.Gradients):
# Preconditioner solves the IS problem; hence, get_init_guess rewrite is
# unnecessary
get_init_guess = sacasscf_grad.Gradients.get_init_guess
project_Aop = sacasscf_grad.Gradients.project_Aop
def __init__(self, mc):
self.conv_rtol = 0
def_tol0 = getattr (mc, 'conv_tol_grad', None)
if def_tol0 is None:
def_tol0 = np.sqrt (getattr (mc, 'conv_tol', 1e-7))
def_tol1 = getattr (mc, 'conv_tol_diabatize',
CONV_TOL_DIABATIZE)
self.conv_atol = min (def_tol0, def_tol1)
self.sing_step_tol = SING_STEP_TOL
mcpdft_grad.Gradients.__init__(self, mc)
r, g = get_diabfns (self.base.diabatization)
self._diab_response = r
self._diab_grad = g
self.nlag += self.nis
@property
def nis (self):
return self.nroots * (self.nroots - 1) // 2
[docs]
def diab_response (self, Lis, **kwargs):
return self._diab_response (self, Lis, **kwargs)
[docs]
def diab_grad (self, Lis, **kwargs):
return self._diab_grad (self, Lis, **kwargs)
[docs]
def kernel (self, state=None, mo=None, ci=None, si=None, _freeze_is=False,
**kwargs):
'''Cache the Hamiltonian and effective Hamiltonian terms, and
pass around the IS hessian
eris, veff1, veff2, and d2f should be available to all top-level
functions: get_wfn_response, get_Aop_Adiag, get_ham_response,
and get_LdotJnuc
freeze_is == True sets the is component of the response to zero
for debugging purposes
'''
if state is None: state = self.state
if mo is None: mo = self.base.mo_coeff
if ci is None: ci = self.base.ci
if si is None: si = self.base.si
if isinstance (ci, np.ndarray): ci = [ci] # hack hack hack...
if state is None:
raise NotImplementedError ('Gradient of PDFT state-average energy')
self.state = state # Not the best code hygiene maybe
nroots = self.nroots
veff1 = []
veff2 = []
d2f = self.base.diabatizer (ci=ci)[2]
for ix in range (nroots):
v1, v2 = self.base.get_pdft_veff (mo, ci, incl_coul=True,
paaa_only=True, state=ix)
veff1.append (v1)
veff2.append (v2)
return mcpdft_grad.Gradients.kernel (self, state=state, mo=mo, ci=ci,
si=si, d2f=d2f, veff1=veff1, veff2=veff2, _freeze_is=_freeze_is,
**kwargs)
[docs]
def pack_uniq_var (self, xorb, xci, xis=None):
x = sacasscf_grad.Gradients.pack_uniq_var (self, xorb, xci)
if xis is not None: x = np.append (x, xis)
return x
[docs]
def unpack_uniq_var (self, x):
ngorb, nci, nis = self.ngorb, self.nci, self.nis
x, xis = x[:ngorb+nci], x[ngorb+nci:]
xorb, xci = sacasscf_grad.Gradients.unpack_uniq_var (self, x)
if len (xis)==nis: return xorb, xci, xis
return xorb, xci
def _get_is_component (self, xci, ci=None, symm=-1):
# TODO: state-average-mix
if ci is None: ci = self.base.ci
nroots = self.nroots
xci = np.asarray (xci).reshape (nroots, -1)
ci = np.asarray (ci).reshape (nroots, -1)
xis = np.dot (xci.conj (), ci.T)
if symm > -1: xis -= xis.T
else:
assert (np.amax (np.abs (xis + xis.T)) < 1e-8), '{}'.format (xis)
return xis[np.tril_indices (nroots, k=-1)]
def _separate_is_component (self, xci, ci=None, symm=-1):
# TODO: state-average-mix
is_list = isinstance (xci, list)
is_tuple = isinstance (xci, tuple)
if ci is None: ci = self.base.ci
nroots = self.nroots
ishape = np.asarray (xci).shape
xci = np.asarray (xci).reshape (nroots, -1)
xci = np.asarray (xci).reshape (nroots, -1)
ci = np.asarray (ci).reshape (nroots, -1)
xis = np.dot (xci.conj (), ci.T)
xci -= np.dot (xis.conj (), ci)
xci = xci.reshape (ishape)
if is_list: xci = list (xci)
elif is_tuple: xci = tuple (xci)
if symm > -1: xis -= xis.T
#else:
# assert (np.amax (np.abs (xis + xis.T)) < 1e-8), '{}'.format (xis)
xis = xis[np.tril_indices (nroots, k=-1)]
return xci, xis
[docs]
def get_wfn_response (self, si_bra=None, si_ket=None, state=None, mo=None,
ci=None, si=None, eris=None, veff1=None, veff2=None,
_freeze_is=False, d2f=None, **kwargs):
if mo is None: mo = self.base.mo_coeff
if ci is None: ci = self.base.ci
if si is None: si = self.base.si
if state is None: state = self.state
ket, bra = _unpack_state (state)
if si_bra is None: si_bra = si[:,bra]
if si_ket is None: si_ket = si[:,ket]
if d2f is None: d2f = self.base.diabatizer (ci=ci)[2]
log = lib.logger.new_logger (self, self.verbose)
si_diag = si_bra * si_ket
# Diagonal: PDFT component
nlag = self.nlag-self.nis
g_all_pdft = np.zeros (nlag)
for i, (amp, c, v1, v2) in enumerate (zip (si_diag, ci, veff1, veff2)):
if not amp: continue
g_i = mcpdft_grad.Gradients.get_wfn_response (self,
state=i, mo=mo, ci=ci, veff1=v1, veff2=v2, nlag=nlag, **kwargs)
g_all_pdft += amp * g_i
if self.verbose >= lib.logger.DEBUG:
g_orb, g_ci = self.unpack_uniq_var (g_i)
g_ci, g_is = self._separate_is_component (g_ci, ci=ci, symm=0)
log.debug ('g_is pdft state {} component:\n{} * {}'.format (i,
amp, g_is))
# DEBUG
g_orb_pdft, g_ci = self.unpack_uniq_var (g_all_pdft)
g_ci, g_is_pdft = self._separate_is_component (g_ci, ci=ci, symm=0)
# Off-diagonal: heff component
g_orb_heff, g_is_heff = mspdft_heff_response (self, mo=mo, ci=ci,
si_bra=si_bra, si_ket=si_ket, eris=eris)
log.debug ('g_is pdft total component:\n{}'.format (g_is_pdft))
log.debug ('g_is heff component:\n{}'.format (g_is_heff))
# Combine
g_orb = g_orb_pdft + g_orb_heff
g_is = g_is_pdft + g_is_heff
if _freeze_is: g_is[:] = 0.0
g_all = self.pack_uniq_var (g_orb, g_ci, g_is)
# DEBUG
d2f_evals, d2f_evecs = linalg.eigh (d2f)
g_is_modes = np.dot (g_is, d2f_evecs)
g_is_pdft = np.dot (g_is_pdft, d2f_evecs)
g_is_heff = np.dot (g_is_heff, d2f_evecs)
log.debug ("IS sector Lagrange multiplier solution:")
for i, (denom, num) in enumerate (zip (d2f_evals, g_is_modes)):
log.debug ('%d %e / %e = %e (%e %e)', i, -num, denom, -num/denom,
g_is_pdft[i], g_is_heff[i])
return g_all
[docs]
def get_Aop_Adiag (self, verbose=None, mo=None, ci=None, eris=None,
level_shift=None, d2f=None, **kwargs):
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 eris is None and self.eris is None:
eris = self.eris = self.base.ao2mo (mo)
elif eris is None:
eris = self.eris
if d2f is None: d2f = self.base.diabatizer (ci=ci)[2]
ham_od = self.base.get_heff_offdiag ()
ham_od += ham_od.T # This corresponds to the arbitrary newton_casscf*2
fcasscf = self.make_fcasscf_sa ()
hop, Adiag = newton_casscf.gen_g_hop (fcasscf, mo, ci, eris,
verbose)[2:]
ngorb, nci = self.ngorb, self.nci
# TODO: cacheing diab_response? or an x=0 branch?
def Aop (x):
x_v, x_is = x[:ngorb+nci], x[ngorb+nci:]
Ax_v = hop (x_v) + self.diab_response (x_is, mo=mo, ci=ci,
eris=eris)
x_c = self.unpack_uniq_var (x_v)[1]
Ax_is = np.dot (d2f, x_is)
Ax_o, Ax_c = self.unpack_uniq_var (Ax_v)
Ax_c, Ax_is2 = self._separate_is_component (Ax_c)
Ax_c_od = list (np.tensordot (-ham_od, np.stack (x_c, axis=0),
axes=1))
Ax_c = [a1 + (w*a2) for a1, a2, w in zip (Ax_c, Ax_c_od,
self.base.weights)]
return self.pack_uniq_var (Ax_o, Ax_c, Ax_is)
return Aop, Adiag
[docs]
def get_lagrange_precond (self, Adiag, level_shift=None, ci=None, d2f=None,
**kwargs):
if level_shift is None: level_shift = self.level_shift
if ci is None: ci = self.base.ci
if d2f is None: d2f = self.base.diabatizer (ci=ci)[2]
return MSPDFTLagPrec (Adiag=Adiag, level_shift=level_shift, ci=ci,
d2f=d2f, grad_method=self)
[docs]
def get_ham_response (self, si_bra=None, si_ket=None, state=None, mo=None,
ci=None, si=None, eris=None, veff1=None, veff2=None, mf_grad=None,
atmlst=None, verbose=None, **kwargs):
'''write mspdft heff Hellmann-Feynman calculator; sum over
diagonal PDFT Hellmann-Feynman terms
'''
if atmlst is None: atmlst = self.atmlst
if mo is None: mo = self.base.mo_coeff
if ci is None: ci = self.base.ci
if si is None: si = self.base.si
if state is None: state = self.state
ket, bra = _unpack_state (state)
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 verbose is None: verbose = self.verbose
si_diag = si_bra * si_ket
log = logger.new_logger (self, verbose)
# Fix messed-up counting of the nuclear part
de_nuc = mf_grad.grad_nuc (self.mol, atmlst)
log.debug ('MS-PDFT gradient n-n terms:\n{}'.format (de_nuc))
de = si_diag.sum () * de_nuc.copy ()
# Diagonal: PDFT component
for i, (amp, c, v1, v2) in enumerate (zip (si_diag, ci, veff1, veff2)):
if not amp: continue
de_i = mcpdft_grad.Gradients.get_ham_response (self, state=i,
mo=mo, ci=ci, veff1=v1, veff2=v2, eris=eris, mf_grad=mf_grad,
verbose=0, **kwargs) - de_nuc
log.debug ('MS-PDFT gradient int-state {} EPDFT terms:\n{}'.format
(i, de_i))
log.debug ('Factor for these terms: {}'.format (amp))
de += amp * de_i
log.debug ('MS-PDFT gradient diag H-F terms:\n{}'.format (de))
# Off-diagonal: heff component
de_o = mspdft_heff_HellmanFeynman (self, mo_coeff=mo, ci=ci,
si_bra=si_bra, si_ket=si_ket, eris=eris, state=state,
mf_grad=mf_grad, **kwargs)
log.debug ('MS-PDFT gradient offdiag H-F terms:\n{}'.format (de_o))
de += de_o
return de
[docs]
def get_LdotJnuc (self, Lvec, atmlst=None, verbose=None, mo=None,
ci=None, eris=None, mf_grad=None, d2f=None, **kwargs):
''' Add the IS component '''
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 eris is None and self.eris is None:
eris = self.eris = self.base.ao2mo (mo)
elif eris is None:
eris = self.eris
ngorb, nci = self.ngorb, self.nci
Lvec_v, Lvec_is = Lvec[:ngorb+nci], Lvec[ngorb+nci:]
# Double-check Lvec_v sanity
Lvec_orb, Lvec_ci = self.unpack_uniq_var (Lvec_v)
Lvec_is2 = self._get_is_component (Lvec_ci, symm=0)
assert (np.amax (np.abs (Lvec_is2)) < 1e-8), '{} {}'.format (Lvec_is,
Lvec_is2)
# Orbital and CI components
de_Lv = sacasscf_grad.Gradients.get_LdotJnuc (self, Lvec_v,
atmlst=atmlst, verbose=verbose, ci=ci, eris=eris, mf_grad=mf_grad,
**kwargs)
# SI component
t0 = (logger.process_clock(), logger.perf_counter())
de_Lis = self.diab_grad (Lvec_is, atmlst=atmlst, mf_grad=mf_grad,
eris=eris, mo=mo, ci=ci, **kwargs)
logger.info (self,
'--------------- %s gradient Lagrange IS response ---------------',
self.base.__class__.__name__)
if verbose >= logger.INFO:
rhf_grad._write(self, self.mol, de_Lis, atmlst)
logger.info (self,
'----------------------------------------------------------------')
t0 = logger.timer (self, '{} gradient Lagrange IS response'.format (
self.base.__class__.__name__), *t0)
return de_Lv + de_Lis
[docs]
def get_lagrange_callback (self, Lvec_last, itvec, geff_op):
# TODO: state-average-mix
log = logger.new_logger (self, self.verbose)
if isinstance (self.base.fcisolver, CSFFCISolver):
transf = self.base.fcisolver.transformer
def _debug_csfs (xci, tag):
xci_csf = transf.vec_det2csf (xci, normalize=False)
xci_p = transf.vec_csf2det (xci_csf, normalize=False)
xci_bs_norm = linalg.norm (np.concatenate (
[x.ravel () - y.ravel () for x, y in zip (xci_p, xci)]))
log.debug ('Broken-spin |{}| = {}'.format (tag, xci_bs_norm))
else:
def _debug_csfs (xci, tag):
pass
def my_call (x):
itvec[0] += 1
geff = geff_op (x)
deltax = x - Lvec_last
gorb, gci, gis = self.unpack_uniq_var (geff)
deltaorb, deltaci, deltais = self.unpack_uniq_var (deltax)
gci_norm = linalg.norm (np.asarray (gci).ravel ())
deltaci_norm = linalg.norm (np.asarray (deltaci).ravel ())
logger.info(self, ('Lagrange optimization iteration {}, |gorb| = '
'{}, |gci| = {}, |gis| = {} |dLorb| = {}, |dLci| = {}, |dLis|'
' = {}').format (itvec[0], linalg.norm (gorb),
gci_norm, linalg.norm (gis), linalg.norm (deltaorb),
deltaci_norm, linalg.norm (deltais)))
_debug_csfs (gci, 'gci')
_debug_csfs (deltaci, 'dLci')
Lvec_last[:] = x[:]
return my_call
[docs]
def debug_lagrange (self, Lvec, bvec, Aop, Adiag, state=None, mo=None,
ci=None, d2f=None, verbose=None, eris=None, **kwargs):
if state is None: state = self.state
if mo is None: mo = self.base.mo_coeff
if ci is None: ci = self.base.ci
if eris is None: eris = self.base.ao2mo (mo)
if verbose is None: verbose = self.verbose
nroots = self.nroots
log = logger.new_logger (self, verbose)
if isinstance (self.base.fcisolver, CSFFCISolver):
transf = self.base.fcisolver.transformer
def _debug_csfs (xci, label, normalize=False):
strs, vecs = transf.printable_largest_csf (np.asarray (xci),
10, isdet=True, normalize=normalize, order='C')
log.debug ('Leading CSFs for %s', label)
for iroot in range (nroots):
log.debug (' Root %d', iroot)
for s, v in zip (strs[iroot], vecs[iroot]):
log.debug (' |%s>: %s', s, v)
else:
def _debug_csfs (*args, **kwargs):
pass
def _debug_cispace (xci, label):
log.debug ('%s', label)
xci_norm = [np.dot (c.ravel (), c.ravel ()) for c in xci]
try:
xci_ss = self.base.fcisolver.states_spin_square (xci,
self.base.ncas, self.base.nelecas)[0]
except AttributeError:
from pyscf.fci.direct_spin1 import _unpack_nelec, spin_square
nelec = sum (_unpack_nelec(self.base.nelecas))
xci_ss = [spin_square (x, self.base.ncas,
((nelec+m)//2,(nelec-m)//2))[0]
for x, m in zip (xci, self.spin_states)]
xci_ss = [x / max (y, 1e-8) for x, y in zip (xci_ss, xci_norm)]
xci_multip = [np.sqrt (x+.25) - .5 for x in xci_ss]
xci_norm = np.sqrt (xci_norm)
for ix, (norm, ss, multip) in enumerate (zip (xci_norm, xci_ss,
xci_multip)):
log.debug ((' State {} norm = {:.7e} ; <S^2> = {:.7f} ; 2S+1'
' = {:.7f}').format (ix, norm, ss, multip))
ovlp = np.zeros ((nroots, nroots), dtype=xci[0].dtype)
for i, j in product (range (nroots), repeat=2):
if self.spin_states[i] != self.spin_states[j]: continue
ovlp[i,j] = np.dot (xci[i].ravel (), ci[j].ravel ())
log.debug (' Overlap matrix with CI array:')
fmt_str = ' ' + ' '.join (['{:8.1e}',]*nroots)
for row in ovlp: log.debug (fmt_str.format (*row))
_debug_csfs (ci, 'CI vector', normalize=True)
borb, bci, bis = self.unpack_uniq_var (bvec)
log.debug ('Orbital rotation gradient (b) norm = {:.6e}'.format (
linalg.norm (borb)))
_debug_cispace (bci, 'CI gradient (b)')
_debug_csfs (bci, 'CI gradient (b)')
Aorb, Aci = self.unpack_uniq_var (Adiag)
log.debug ('Orbital rotation Hessian (A) diagonal norm = {:.7e}'.format
(linalg.norm (Aorb)))
_debug_cispace (Aci, 'CI Hessian (A) diagonal')
_debug_csfs (Aci, 'CI Hessian (A) diagonal')
Lorb, Lci, Lis = self.unpack_uniq_var (Lvec)
log.debug ('Orbital rotation Lagrange vector (x) norm = {:.7e}'.format
(linalg.norm (Lorb)))
_debug_cispace (Lci, 'CI Lagrange (x) vector')
_debug_csfs (Lci, 'CI Lagrange (x) vector')
log.debug ('{} Constraint Jacobian (A):'.format (self.base.__class__.__name__))
fmt = ' ' + ' '.join (['{:12.5e}' for i in range (self.nis)])
for row in d2f: log.debug (fmt.format (*row))
log.debug (' {:>12s} {:>12s}'.format ('Gradient (b)', 'Vector (x)'))
for g, v in zip (bis, Lis):
log.debug (' {:12.5e} {:12.5e}'.format (g, v))
[docs]
class MSPDFTLagPrec (sacasscf_grad.SACASLagPrec):
''' Solve IS part exactly, then do everything else the same '''
def __init__(self, Adiag=None, level_shift=None, ci=None, grad_method=None,
d2f=None, **kwargs):
sacasscf_grad.SACASLagPrec.__init__(self, Adiag=Adiag,
level_shift=level_shift, ci=ci, grad_method=grad_method)
self.grad_method = grad_method
self.sing_tol = getattr (grad_method.base, 'sing_tol_diabatize',
SING_TOL_DIABATIZE)
self.sing_step_tol = getattr (grad_method.base, 'sing_step_tol',
SING_STEP_TOL)
self.sing_warned = False
self.log = logger.new_logger (self.grad_method,
self.grad_method.verbose)
self._init_d2f (d2f=d2f, **kwargs)
self.verbose = self.grad_method.verbose
def _init_d2f (self, d2f=None, **kwargs):
self.d2f=d2f
self.d2f_evals, self.d2f_evecs = linalg.eigh (d2f)
idx_sing = np.abs (self.d2f_evals) < self.sing_tol
self.log.debug ('IS component Hessian eigenvalues: {}'.format (
self.d2f_evals))
if np.any (idx_sing): self.do_sing_warn ()
self.d2f_evals = self.d2f_evals[~idx_sing]
self.d2f_evecs = self.d2f_evecs[:,~idx_sing]
[docs]
def unpack_uniq_var (self, x):
return self.grad_method.unpack_uniq_var (x)
[docs]
def pack_uniq_var (self, x0, x1, x2=None):
return self.grad_method.pack_uniq_var (x0, x1, x2)
def __call__(self, x):
xorb, xci, xis = self.unpack_uniq_var (x)
Mxorb = self.orb_prec (xorb)
Mxci = self.ci_prec (xci)
Mxis = self.is_prec (xis)
return self.pack_uniq_var (Mxorb, Mxci, Mxis)
[docs]
def is_prec (self, xis):
xis = np.dot (xis, self.d2f_evecs)
Mxis = xis/self.d2f_evals
idx_sing = (np.abs (Mxis) >= self.sing_step_tol)
if np.any (idx_sing): self.do_sing_warn ()
Mxis[idx_sing] = 0
Mxis = np.dot (self.d2f_evecs, Mxis)
return Mxis
[docs]
def do_sing_warn (self):
if self.sing_warned: return
self.log.warn ('Model-space frame-rotation Hessian is singular! '
'Response equations may not be solvable to arbitrary '
'precision!')
self.sing_warned = True
if __name__ == '__main__':
# Test mspdft_heff_response and mspdft_heff_HellmannFeynman by trying to
# reproduce SA-CASSCF derivatives in an arbitrary basis
import math
from pyscf import scf, gto, mcscf
from pyscf.fci import csf_solver
xyz = '''O 0.00000000 0.08111156 0.00000000
H 0.78620605 0.66349738 0.00000000
H -0.78620605 0.66349738 0.00000000'''
mol = gto.M (atom=xyz, basis='6-31g', symmetry=False, output='mspdft.log',
verbose=lib.logger.DEBUG)
mf = scf.RHF (mol).run ()
mc = mcpdft.CASSCF (mf, 'tPBE', 4, 4).set (fcisolver = csf_solver (mol, 1))
mc = mc.multi_state ([1.0/3,]*3, 'cms').run ()
mc_grad = Gradients (mc)
de = np.stack ([mc_grad.kernel (state=i) for i in range (3)], axis=0)