#!/usr/bin/env python
# Copyright 2014-2021 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 gc
import numpy as np
from functools import reduce
from itertools import product
from scipy import linalg
from pyscf import gto
from pyscf.grad import lagrange
from pyscf.mcscf.addons import StateAverageMCSCFSolver, StateAverageFCISolver
from pyscf.mcscf.addons import StateAverageMixFCISolver, state_average_mix_
from pyscf.grad.mp2 import _shell_prange
from pyscf.mcscf import mc1step, mc1step_symm, newton_casscf
from pyscf.grad import casscf as casscf_grad
from pyscf.grad import rhf as rhf_grad
from pyscf.fci.direct_spin1 import _unpack_nelec
from pyscf.fci.addons import fix_spin_, SpinPenaltyFCISolver
from pyscf.fci.spin_op import spin_square
from pyscf.fci import cistring
from pyscf.lib import logger
from pyscf import lib, ao2mo, mcscf
# ref. Mol. Phys., 99, 103 (2001); DOI: 10.1080/002689700110005642
[docs]
def Lorb_dot_dgorb_dx (Lorb, mc, mo_coeff=None, ci=None, atmlst=None, mf_grad=None, eris=None,
verbose=None):
''' Modification of single-state CASSCF electronic energy nuclear gradient to compute instead
the orbital Lagrange term nuclear gradient:
sum_pq Lorb_pq d2_Ecas/d_lambda d_kpq
This involves the effective density matrices
~D_pq = L_pr*D_rq + L_qr*D_pr
~d_pqrs = L_pt*d_tqrs + L_rt*d_pqts + L_qt*d_ptrs + L_st*d_pqrt
(NB: L_pq = -L_qp)
'''
# dmo = smoT.dao.smo
# dao = mo.dmo.moT
t0 = (logger.process_clock(), logger.perf_counter())
if mo_coeff is None: mo_coeff = mc.mo_coeff
if ci is None: ci = mc.ci
if mf_grad is None: mf_grad = mc._scf.nuc_grad_method()
if mc.frozen is not None:
raise NotImplementedError
mol = mc.mol
ncore = mc.ncore
ncas = mc.ncas
nocc = ncore + ncas
nelecas = mc.nelecas
nao, nmo = mo_coeff.shape
nao_pair = nao * (nao+1) // 2
mo_core = mo_coeff[:,:ncore]
mo_cas = mo_coeff[:,ncore:nocc]
# MRH: new 'effective' MO coefficients including contraction from the Lagrange multipliers
moL_coeff = np.dot (mo_coeff, Lorb)
s0_inv = np.dot (mo_coeff, mo_coeff.T)
moL_core = moL_coeff[:,:ncore]
moL_cas = moL_coeff[:,ncore:nocc]
# MRH: these SHOULD be state-averaged! Use the actual sacasscf object!
casdm1, casdm2 = mc.fcisolver.make_rdm12(ci, ncas, nelecas)
# gfock = Generalized Fock, Adv. Chem. Phys., 69, 63
dm_core = np.dot(mo_core, mo_core.T) * 2
dm_cas = reduce(np.dot, (mo_cas, casdm1, mo_cas.T))
# MRH: new density matrix terms
dmL_core = np.dot(moL_core, mo_core.T) * 2
dmL_cas = reduce(np.dot, (moL_cas, casdm1, mo_cas.T))
dmL_core += dmL_core.T
dmL_cas += dmL_cas.T
dm1 = dm_core + dm_cas
dm1L = dmL_core + dmL_cas
# MRH: wrap the integral instead of the density matrix.
# g_prst*~d_qrst = (g_pust*L_ur + g_prut*L_us + g_prsu*L_ut)*d_qrst + g_prst*L_uq*d_urst
# = 'aapaL'_prst*d_qrst [ERI TERM 1]
# = 'aapa'_prst*L_uq*d_urst [ERI TERM 2]
aapa = np.zeros ((ncas, ncas, nmo, ncas), dtype=dm_cas.dtype)
aapaL = np.zeros ((ncas, ncas, nmo, ncas), dtype=dm_cas.dtype)
for i in range (nmo):
jbuf = eris.ppaa[i]
kbuf = eris.papa[i]
aapa[:,:,i,:] = jbuf[ncore:nocc,:,:].transpose (1,2,0)
aapaL[:,:,i,:] += np.tensordot (jbuf, Lorb[:,ncore:nocc], axes=((0),(0)))
kbuf = np.tensordot (kbuf, Lorb[:,ncore:nocc], axes=((1),(0))).transpose (1,2,0)
aapaL[:,:,i,:] += kbuf + kbuf.transpose (1,0,2)
# MRH: new vhf terms
vj, vk = mc._scf.get_jk(mol, (dm_core, dm_cas))
vjL, vkL = mc._scf.get_jk(mol, (dmL_core, dmL_cas))
h1 = mc.get_hcore()
vhf_c = vj[0] - vk[0] * .5
vhf_a = vj[1] - vk[1] * .5
vhfL_c = vjL[0] - vkL[0] * .5
vhfL_a = vjL[1] - vkL[1] * .5
gfock = np.dot (h1, dm1L) # h1e
gfock += np.dot ((vhf_c + vhf_a), dmL_core) # core-core and active-core, 2nd 1RDM linked
gfock += np.dot ((vhfL_c + vhfL_a), dm_core) # core-core and active-core, 1st 1RDM linked
gfock += np.dot (vhfL_c, dm_cas) # core-active, 1st 1RDM linked
gfock += np.dot (vhf_c, dmL_cas) # core-active, 2nd 1RDM linked
gfock = np.dot (s0_inv, gfock) # Definition in MO's; going (AO->MO->AO) incurs inverse ovlp
# [ERI TERM 1]
gfock += reduce (np.dot, (mo_coeff, np.einsum('uviw,uvtw->it', aapaL, casdm2), mo_cas.T))
# [ERI TERM 2]
gfock += reduce (np.dot, (mo_coeff, np.einsum('uviw,vuwt->it', aapa, casdm2), moL_cas.T))
dme0 = (gfock+gfock.T)/2 # This transpose is for the overlap matrix later on
aapa = vj = vk = vhf_c = vhf_a = None
vj, vk = mf_grad.get_jk (mol, (dm_core, dm_cas, dmL_core, dmL_cas))
vhf1c, vhf1a, vhf1cL, vhf1aL = vj - vk * 0.5
hcore_deriv = mf_grad.hcore_generator(mol)
s1 = mf_grad.get_ovlp(mol)
diag_idx = np.arange(nao)
diag_idx = diag_idx * (diag_idx+1) // 2 + diag_idx
casdm2_cc = casdm2 + casdm2.transpose(0,1,3,2)
dm2buf = ao2mo._ao2mo.nr_e2(casdm2_cc.reshape(ncas**2,ncas**2), mo_cas.T,
(0, nao, 0, nao)).reshape(ncas**2,nao,nao)
# MRH: contract the final two indices of the active-active 2RDM with L as you change to AOs
# note tensordot always puts indices in the order of the arguments.
dm2Lbuf = np.zeros ((ncas**2,nmo,nmo))
# MRH: The second line below transposes the L; the third line transposes the derivative
# Both the L and the derivative have to explore all indices
Lcasdm2 = np.tensordot (Lorb[:,ncore:nocc], casdm2, axes=(1,2)).transpose (1,2,0,3)
dm2Lbuf[:,:,ncore:nocc] = Lcasdm2.reshape (ncas**2,nmo,ncas)
Lcasdm2 = np.tensordot (Lorb[:,ncore:nocc], casdm2, axes=(1,3)).transpose (1,2,3,0)
dm2Lbuf[:,ncore:nocc,:] += Lcasdm2.reshape (ncas**2,ncas,nmo)
Lcasdm2 = None
dm2Lbuf += dm2Lbuf.transpose (0,2,1)
dm2Lbuf = np.ascontiguousarray (dm2Lbuf)
dm2Lbuf = ao2mo._ao2mo.nr_e2(dm2Lbuf.reshape (ncas**2,nmo**2), mo_coeff.T,
(0, nao, 0, nao)).reshape(ncas**2,nao,nao)
dm2buf = lib.pack_tril(dm2buf)
dm2buf[:,diag_idx] *= .5
dm2buf = dm2buf.reshape(ncas,ncas,nao_pair)
dm2Lbuf = lib.pack_tril(dm2Lbuf)
dm2Lbuf[:,diag_idx] *= .5
dm2Lbuf = dm2Lbuf.reshape(ncas,ncas,nao_pair)
if atmlst is None:
atmlst = list (range(mol.natm))
aoslices = mol.aoslice_by_atom()
de_hcore = np.zeros((len(atmlst),3))
de_renorm = np.zeros((len(atmlst),3))
de_eri = np.zeros((len(atmlst),3))
de = np.zeros((len(atmlst),3))
max_memory = mc.max_memory - lib.current_memory()[0]
blksize = int(max_memory*.9e6/8 / (4*(aoslices[:,3]-aoslices[:,2]).max()*nao_pair))
# MRH: 3 components of eri array and 1 density matrix array:
# FOUR arrays of this size are required!
blksize = min(nao, max(2, blksize))
logger.info (mc, 'SA-CASSCF Lorb_dot_dgorb memory remaining for eri manipulation: %f MB; using'
' blocksize = %d', max_memory, blksize)
t0 = logger.timer (mc, 'SA-CASSCF Lorb_dot_dgorb 1-electron part', *t0)
for k, ia in enumerate(atmlst):
shl0, shl1, p0, p1 = aoslices[ia]
h1ao = hcore_deriv(ia)
# MRH: h1e and Feff terms
de_hcore[k] += np.einsum('xij,ij->x', h1ao, dm1L)
de_renorm[k] -= np.einsum('xij,ij->x', s1[:,p0:p1], dme0[p0:p1]) * 2
q1 = 0
for b0, b1, nf in _shell_prange(mol, 0, mol.nbas, blksize):
q0, q1 = q1, q1 + nf
dm2_ao = lib.einsum('ijw,pi,qj->pqw', dm2Lbuf, mo_cas[p0:p1], mo_cas[q0:q1])
# MRH: contract first two indices of active-active 2RDM with L as you go MOs -> AOs
dm2_ao += lib.einsum('ijw,pi,qj->pqw', dm2buf, moL_cas[p0:p1], mo_cas[q0:q1])
dm2_ao += lib.einsum('ijw,pi,qj->pqw', dm2buf, mo_cas[p0:p1], moL_cas[q0:q1])
shls_slice = (shl0,shl1,b0,b1,0,mol.nbas,0,mol.nbas)
eri1 = mol.intor('int2e_ip1', comp=3, aosym='s2kl',
shls_slice=shls_slice).reshape(3,p1-p0,nf,nao_pair)
# MRH: I still don't understand why there is a minus here!
de_eri[k] -= np.einsum('xijw,ijw->x', eri1, dm2_ao) * 2
eri1 = dm2_ao = None
t0 = logger.timer (mc, 'SA-CASSCF Lorb_dot_dgorb atom {} ({},{}|{})'.format (ia, p1-p0,
nf, nao_pair), *t0)
# MRH: core-core and core-active 2RDM terms
de_eri[k] += np.einsum('xij,ij->x', vhf1c[:,p0:p1], dm1L[p0:p1]) * 2
de_eri[k] += np.einsum('xij,ij->x', vhf1cL[:,p0:p1], dm1[p0:p1]) * 2
# MRH: active-core 2RDM terms
de_eri[k] += np.einsum('xij,ij->x', vhf1a[:,p0:p1], dmL_core[p0:p1]) * 2
de_eri[k] += np.einsum('xij,ij->x', vhf1aL[:,p0:p1], dm_core[p0:p1]) * 2
# MRH: deleted the nuclear-nuclear part to avoid double-counting
# lesson learned from debugging - mol.intor computes -1 * the derivative and only
# for one index
# on the other hand, mf_grad.hcore_generator computes the actual derivative of
# h1 for both indices and with the correct sign
logger.debug (mc, "Orb lagrange hcore component:\n{}".format (de_hcore))
logger.debug (mc, "Orb lagrange renorm component:\n{}".format (de_renorm))
logger.debug (mc, "Orb lagrange eri component:\n{}".format (de_eri))
de = de_hcore + de_renorm + de_eri
return de
[docs]
def Lci_dot_dgci_dx (Lci, weights, mc, mo_coeff=None, ci=None, atmlst=None, mf_grad=None,
eris=None, verbose=None):
''' Modification of single-state CASSCF electronic energy nuclear gradient to compute instead
the CI Lagrange term nuclear gradient:
sum_IJ Lci_IJ d2_Ecas/d_lambda d_PIJ
This involves the effective density matrices
~D_pq = sum_I w_I<L_I|p'q|I> + c.c.
~d_pqrs = sum_I w_I<L_I|p'r'sq|I> + c.c.
(NB: All-core terms ~D_ii, ~d_iijj = 0
However, active-core terms ~d_xyii, ~d_xiiy != 0)
'''
if mo_coeff is None: mo_coeff = mc.mo_coeff
if ci is None: ci = mc.ci
if mf_grad is None: mf_grad = mc._scf.nuc_grad_method()
if mc.frozen is not None:
raise NotImplementedError
t0 = (logger.process_clock(), logger.perf_counter())
mol = mc.mol
ncore = mc.ncore
ncas = mc.ncas
nocc = ncore + ncas
nelecas = mc.nelecas
nao, nmo = mo_coeff.shape
nao_pair = nao * (nao+1) // 2
mo_occ = mo_coeff[:,:nocc]
mo_core = mo_coeff[:,:ncore]
mo_cas = mo_coeff[:,ncore:nocc]
# MRH: TDMs + c.c. instead of RDMs
# MRH, 06/30/2020: new interface in mcscf.addons makes this much more transparent
casdm1, casdm2 = mc.fcisolver.trans_rdm12 (Lci, ci, ncas, nelecas)
casdm1 += casdm1.transpose (1,0)
casdm2 += casdm2.transpose (1,0,3,2)
# gfock = Generalized Fock, Adv. Chem. Phys., 69, 63
dm_core = np.dot(mo_core, mo_core.T) * 2
dm_cas = reduce(np.dot, (mo_cas, casdm1, mo_cas.T))
aapa = np.zeros ((ncas, ncas, nmo, ncas), dtype=dm_cas.dtype)
for i in range (nmo):
aapa[:,:,i,:] = eris.ppaa[i][ncore:nocc,:,:].transpose (1,2,0)
vj, vk = mc._scf.get_jk(mol, (dm_core, dm_cas))
h1 = mc.get_hcore()
vhf_c = vj[0] - vk[0] * .5
vhf_a = vj[1] - vk[1] * .5
# MRH: delete h1 + vhf_c from the first line below (core and core-core stuff)
# Also extend gfock to span the whole space
gfock = np.zeros_like (dm_cas)
gfock[:,:nocc] = reduce(np.dot, (mo_coeff.T, vhf_a, mo_occ)) * 2
gfock[:,ncore:nocc] = reduce(np.dot, (mo_coeff.T, h1 + vhf_c, mo_cas, casdm1))
gfock[:,ncore:nocc] += np.einsum('uvpw,vuwt->pt', aapa, casdm2)
dme0 = reduce(np.dot, (mo_coeff, (gfock+gfock.T)*.5, mo_coeff.T))
aapa = vj = vk = vhf_c = vhf_a = h1 = gfock = None
vj, vk = mf_grad.get_jk (mol, (dm_core, dm_cas))
vhf1c, vhf1a = vj - vk * 0.5
#vhf1c, vhf1a = mf_grad.get_veff(mol, (dm_core, dm_cas))
hcore_deriv = mf_grad.hcore_generator(mol)
s1 = mf_grad.get_ovlp(mol)
diag_idx = np.arange(nao)
diag_idx = diag_idx * (diag_idx+1) // 2 + diag_idx
casdm2_cc = casdm2 + casdm2.transpose(0,1,3,2)
dm2buf = ao2mo._ao2mo.nr_e2(casdm2_cc.reshape(ncas**2,ncas**2), mo_cas.T,
(0, nao, 0, nao)).reshape(ncas**2,nao,nao)
dm2buf = lib.pack_tril(dm2buf)
dm2buf[:,diag_idx] *= .5
dm2buf = dm2buf.reshape(ncas,ncas,nao_pair)
casdm2 = casdm2_cc = None
if atmlst is None:
atmlst = range(mol.natm)
aoslices = mol.aoslice_by_atom()
de_hcore = np.zeros((len(atmlst),3))
de_renorm = np.zeros((len(atmlst),3))
de_eri = np.zeros((len(atmlst),3))
de = np.zeros((len(atmlst),3))
max_memory = mc.max_memory - lib.current_memory()[0]
blksize = int(max_memory*.9e6/8 / (4*(aoslices[:,3]-aoslices[:,2]).max()*nao_pair))
# MRH: 3 components of eri array and 1 density matrix array:
# FOUR arrays of this size are required!
blksize = min(nao, max(2, blksize))
logger.info (mc, 'SA-CASSCF Lci_dot_dgci memory remaining for eri manipulation: %f MB; using '
'blocksize = %d', max_memory, blksize)
t0 = logger.timer (mc, 'SA-CASSCF Lci_dot_dgci 1-electron part', *t0)
for k, ia in enumerate(atmlst):
shl0, shl1, p0, p1 = aoslices[ia]
h1ao = hcore_deriv(ia)
# MRH: dm1 -> dm_cas in the line below
de_hcore[k] += np.einsum('xij,ij->x', h1ao, dm_cas)
de_renorm[k] -= np.einsum('xij,ij->x', s1[:,p0:p1], dme0[p0:p1]) * 2
q1 = 0
for b0, b1, nf in _shell_prange(mol, 0, mol.nbas, blksize):
q0, q1 = q1, q1 + nf
dm2_ao = lib.einsum('ijw,pi,qj->pqw', dm2buf, mo_cas[p0:p1], mo_cas[q0:q1])
shls_slice = (shl0,shl1,b0,b1,0,mol.nbas,0,mol.nbas)
eri1 = mol.intor('int2e_ip1', comp=3, aosym='s2kl',
shls_slice=shls_slice).reshape(3,p1-p0,nf,nao_pair)
de_eri[k] -= np.einsum('xijw,ijw->x', eri1, dm2_ao) * 2
eri1 = dm2_ao = None
t0 = logger.timer (mc, 'SA-CASSCF Lci_dot_dgci atom {} ({},{}|{})'.format (ia, p1-p0,
nf, nao_pair), *t0)
# MRH: dm1 -> dm_cas in the line below. Also eliminate core-core terms
de_eri[k] += np.einsum('xij,ij->x', vhf1c[:,p0:p1], dm_cas[p0:p1]) * 2
de_eri[k] += np.einsum('xij,ij->x', vhf1a[:,p0:p1], dm_core[p0:p1]) * 2
logger.debug (mc, "CI lagrange hcore component:\n{}".format (de_hcore))
logger.debug (mc, "CI lagrange renorm component:\n{}".format (de_renorm))
logger.debug (mc, "CI lagrange eri component:\n{}".format (de_eri))
de = de_hcore + de_renorm + de_eri
return de
[docs]
def as_scanner(mcscf_grad, state=None):
'''Generating a nuclear gradients scanner/solver (for geometry optimizer).
The returned solver is a function. This function requires one argument
"mol" as input and returns energy and first order nuclear derivatives.
The solver will automatically use the results of last calculation as the
initial guess of the new calculation. All parameters assigned in the
nuc-grad object and SCF object (DIIS, conv_tol, max_memory etc) are
automatically applied in the solver.
Note scanner has side effects. It may change many underlying objects
(_scf, with_df, with_x2c, ...) during calculation.
Examples:
>>> from pyscf import gto, scf, mcscf
>>> mol = gto.M(atom='N 0 0 0; N 0 0 1.1', verbose=0)
>>> mc_grad_scanner = mcscf.CASSCF(scf.RHF(mol), 4, 4).nuc_grad_method().as_scanner()
>>> etot, grad = mc_grad_scanner(gto.M(atom='N 0 0 0; N 0 0 1.1'))
>>> etot, grad = mc_grad_scanner(gto.M(atom='N 0 0 0; N 0 0 1.5'))
'''
if isinstance(mcscf_grad, lib.GradScanner):
return mcscf_grad
#if state is None and (not hasattr (mcscf_grad, 'state') or (mcscf_grad.state is None)):
# return casscf_grad.as_scanner (mcscf_grad)
logger.info(mcscf_grad, 'Create scanner for %s', mcscf_grad.__class__)
name = mcscf_grad.__class__.__name__ + CASSCF_GradScanner.__name_mixin__
return lib.set_class(CASSCF_GradScanner(mcscf_grad, state),
(CASSCF_GradScanner, mcscf_grad.__class__), name)
[docs]
class CASSCF_GradScanner(lib.GradScanner):
def __init__(self, g, state):
lib.GradScanner.__init__(self, g)
if state is None:
self.state = g.state
else:
self.state = state
self._converged = False
def __call__(self, mol_or_geom, **kwargs):
if isinstance(mol_or_geom, gto.MoleBase):
assert mol_or_geom.__class__ == gto.Mole
mol = mol_or_geom
else:
mol = self.mol.set_geom_(mol_or_geom, inplace=False)
self.reset(mol)
if 'state' in kwargs: self.state = kwargs['state']
mc_scanner = self.base
e_tot = mc_scanner(mol)
if hasattr (mc_scanner, 'e_mcscf'): self.e_mcscf = mc_scanner.e_mcscf
if hasattr (mc_scanner, 'e_states') and self.state is not None:
e_tot = mc_scanner.e_states[self.state]
if not ('state' in kwargs):
kwargs['state'] = self.state
de = self.kernel(**kwargs)
return e_tot, de
@property
def converged(self):
return self._converged
@converged.setter
def converged(self, x):
self._converged = x
[docs]
class Gradients (lagrange.Gradients):
_keys = {
'ngorb', 'nroots', 'spin_states', 'na_states', 'nb_states', 'nroots',
'nci', 'state', 'eris', 'weights', 'e_states', 'max_cycle', 'ncas',
'e_cas', 'nelecas', 'mo_occ', 'mo_energy', 'mo_coeff', 'callback',
'chkfile', 'nlag', 'frozen', 'level_shift', 'extrasym', 'fcisolver',
}
def __init__(self, mc, state=None):
self.__dict__.update (mc.__dict__)
nmo = mc.mo_coeff.shape[-1]
self.ngorb = np.count_nonzero (mc.uniq_var_indices (nmo, mc.ncore, mc.ncas, mc.frozen))
self.nroots = mc.fcisolver.nroots
neleca, nelecb = _unpack_nelec (mc.nelecas)
self.spin_states = [neleca - nelecb,] * self.nroots
self.na_states = [cistring.num_strings (mc.ncas, neleca),] * self.nroots
self.nb_states = [cistring.num_strings (mc.ncas, nelecb),] * self.nroots
if isinstance (mc.fcisolver, StateAverageMixFCISolver):
self.nroots = p0 = 0
for solver in mc.fcisolver.fcisolvers:
self.nroots += solver.nroots
nea, neb = mc.fcisolver._get_nelec (solver, (neleca, nelecb))
nstr_a = cistring.num_strings (mc.ncas, nea)
nstr_b = cistring.num_strings (mc.ncas, neb)
for p1 in range (p0, self.nroots):
self.spin_states[p1] = nea - neb
self.na_states[p1] = nstr_a
self.nb_states[p1] = nstr_b
p0 = self.nroots
self.nci = sum ([na * nb for na, nb in zip (self.na_states, self.nb_states)])
if state is not None:
self.state = state
elif hasattr (mc, 'nuc_grad_state'):
self.state = mc.nuc_grad_state
else:
self.state = None
self.eris = None
self.weights = np.array ([1])
try:
self.e_states = np.asarray (mc.e_states)
except AttributeError:
self.e_states = np.asarray (mc.e_tot)
if isinstance (mc, StateAverageMCSCFSolver):
self.weights = np.asarray (mc.weights)
assert (len (self.weights) == self.nroots), '{} {} {}'.format (
mc.fcisolver.__class__, self.weights, self.nroots)
lagrange.Gradients.__init__(self, mc, self.ngorb+self.nci)
self.max_cycle = mc.max_cycle_macro
[docs]
def pack_uniq_var (self, xorb, xci):
# TODO: point-group symmetry of the xci components? CSFs?
xorb = self.base.pack_uniq_var (xorb)
xci = np.concatenate ([x.ravel () for x in xci])
return np.append (xorb, xci)
[docs]
def unpack_uniq_var (self, x):
# TODO: point-group symmetry of the xci components? CSFs?
xorb, x = self.base.unpack_uniq_var (x[:self.ngorb]), x[self.ngorb:]
xci = []
for na, nb in zip (self.na_states, self.nb_states):
xci.append (x[:na*nb].reshape (na, nb))
x = x[na*nb:]
return xorb, xci
[docs]
def make_fcasscf (self, state=None, casscf_attr={}, fcisolver_attr={}):
''' SA-CASSCF nuclear gradients require 1) first derivatives wrt wave function variables
and nuclear shifts of the target state's energy, AND 2) first and second derivatives of the
objective function used to determine the MO coefficients and CI vectors. This function
addresses 1).
Kwargs:
state : integer
The specific state whose energy is being differentiated. This kwarg is necessary
in the context of state_average_mix, where the number of electrons and the
make_rdm* functions differ from state to state.
casscf_attr : dictionary
Extra attributes to apply to fcasscf. Relevant to child methods (i.e., MC-PDFT;
NACs)
fcisolver_attr : dictionary
Extra attributes to apply to fcasscf.fcisolver. Relevant to child methods (i.e.,
MC-PDFT; NACs)
Returns:
fcasscf : object of :class:`mc1step.CASSCF`
Set up to evaluate first derivatives of state "state". Only functions, classes,
and the nelecas variable are set up; the caller should assign MO coefficients
and CI vectors explicitly post facto.
'''
fcasscf = mcscf.CASSCF (self.base._scf, self.base.ncas, self.base.nelecas)
fcasscf.__dict__.update (self.base.__dict__)
nelecas = self.base.nelecas
if isinstance (fcasscf.fcisolver, StateAverageFCISolver):
if isinstance (fcasscf.fcisolver, StateAverageMixFCISolver):
p0 = 0
for solver in fcasscf.fcisolver.fcisolvers:
p1 = p0 + solver.nroots
if p0 <= state < p1:
solver_class = solver.__class__
solver_obj = solver
nelecas = fcasscf.fcisolver._get_nelec (solver_obj, nelecas)
break
p0 = p1
else:
solver_class = self.base.fcisolver._base_class
solver_obj = self.base.fcisolver
fcasscf.fcisolver = solver_obj.view(solver_class)
fcasscf.fcisolver.nroots = 1
# Spin penalty method is inapplicable to response calc'ns
# It must be deactivated for Lagrange multipliers to converge
if isinstance (fcasscf.fcisolver, SpinPenaltyFCISolver):
fcasscf.fcisolver = fcasscf.fcisolver.undo_fix_spin()
fcasscf.__dict__.update (casscf_attr)
fcasscf.nelecas = nelecas
fcasscf.fcisolver.__dict__.update (fcisolver_attr)
fcasscf.verbose, fcasscf.stdout = self.verbose, self.stdout
fcasscf._tag_gfock_ov_nonzero = True
return fcasscf
[docs]
def make_fcasscf_sa (self, casscf_attr={}, fcisolver_attr={}):
''' SA-CASSCF nuclear gradients require 1) first derivatives wrt wave function variables
and nuclear shifts of the target state's energy, AND 2) first and second derivatives of the
objective function used to determine the MO coefficients and CI vectors. This function
addresses 2). Note that penalty methods etc. must be removed, and that child methods such
as MC-PDFT which do not reoptimize the orbitals also do not alter this function.
Kwargs:
casscf_attr : dictionary
Extra attributes to apply to fcasscf. Just in case.
fcisolver_attr : dictionary
Extra attributes to apply to fcasscf.fcisolver. Just in case.
Returns:
fcasscf : object of :class:`StateAverageMCSCFSolver`
Set up to evaluate second derivatives of SA-CASSCF average energy in the
absence of (i.e., spin) penalties.
'''
fcasscf = self.make_fcasscf (state=0, casscf_attr={}, fcisolver_attr={})
fcasscf.__dict__.update (self.base.__dict__)
if isinstance (self.base, StateAverageMCSCFSolver):
if isinstance (self.base.fcisolver, StateAverageMixFCISolver):
fcisolvers = [f.copy() for f in self.base.fcisolver.fcisolvers]
# Spin penalty method is inapplicable to response calc'ns
# It must be deactivated for Lagrange multipliers to converge
for i in range (len (fcisolvers)):
if isinstance (fcisolvers[i], SpinPenaltyFCISolver):
fcisolvers[i].ss_penalty = 0
fcasscf = state_average_mix_(fcasscf, fcisolvers,
self.base.weights)
else:
fcasscf.state_average_(self.base.weights)
# Spin penalty method is inapplicable to response calc'ns
# It must be deactivated for Lagrange multipliers to converge
if isinstance (fcasscf.fcisolver, SpinPenaltyFCISolver):
fcasscf.fcisolver = fcasscf.fcisolver.copy()
fcasscf.fcisolver.ss_penalty = 0
fcasscf.__dict__.update (casscf_attr)
fcasscf.fcisolver.__dict__.update (fcisolver_attr)
return fcasscf
[docs]
def kernel (self, state=None, atmlst=None, verbose=None, mo=None, ci=None, eris=None,
mf_grad=None, e_states=None, level_shift=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 eris is None:
eris = self.eris = self.base.ao2mo (mo)
if mf_grad is None: mf_grad = self.base._scf.nuc_grad_method ()
if state is None:
return casscf_grad.Gradients (self.base).kernel (
mo_coeff=mo, ci=ci, atmlst=atmlst, verbose=verbose)
if e_states is None:
try:
e_states = self.e_states = np.asarray (self.base.e_states)
except AttributeError:
e_states = self.e_states = np.asarray (self.base.e_tot)
if level_shift is None: level_shift=self.level_shift
return lagrange.Gradients.kernel (
self, state=state, atmlst=atmlst, verbose=verbose, mo=mo, ci=ci, eris=eris,
mf_grad=mf_grad, e_states=e_states, level_shift=level_shift, **kwargs)
[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
ndet = self.na_states[state] * self.nb_states[state]
fcasscf = self.make_fcasscf (state)
fcasscf.mo_coeff = mo
fcasscf.ci = ci[state]
eris = fcasscf.ao2mo (mo)
g_all_state = newton_casscf.gen_g_hop (fcasscf, mo, ci[state], eris, verbose)[0]
g_all = np.zeros (self.nlag)
g_all[:self.ngorb] = g_all_state[:self.ngorb]
# No need to reshape or anything, just use the magic of repeated slicing
offs = sum ([na * nb for na, nb in zip(self.na_states[:state],
self.nb_states[:state])]) if state > 0 else 0
g_all[self.ngorb:][offs:][:ndet] = g_all_state[self.ngorb:]
return g_all
[docs]
def get_Aop_Adiag (self, atmlst=None, state=None, verbose=None, mo=None, ci=None, eris=None,
level_shift=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 eris is None and self.eris is None:
eris = self.eris = self.base.ao2mo (mo)
elif eris is None:
eris = self.eris
if not isinstance (self.base, StateAverageMCSCFSolver) and isinstance (ci, list):
ci = ci[0]
fcasscf = self.make_fcasscf_sa ()
Aop, Adiag = newton_casscf.gen_g_hop (fcasscf, mo, ci, eris, verbose)[2:]
# Eliminate the component of Aop (x) which is parallel to the state-average space
# The Lagrange multiplier equations are not defined there
return self.project_Aop (Aop, ci, state), Adiag
[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 eris is None and self.eris is None:
eris = self.eris = self.base.ao2mo (mo)
elif eris is None:
eris = self.eris
fcasscf_grad = casscf_grad.Gradients (self.make_fcasscf (state))
# Mute some misleading messages
fcasscf_grad._finalize = lambda: None
return fcasscf_grad.kernel (mo_coeff=mo, ci=ci[state], atmlst=atmlst, verbose=verbose)
[docs]
def get_LdotJnuc (self, Lvec, 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[state]
if eris is None and self.eris is None:
eris = self.eris = self.base.ao2mo (mo)
elif eris is None:
eris = self.eris
# Just sum the weights now... Lorb can be implicitly summed
# Lci may be in the csf basis
Lorb, Lci = self.unpack_uniq_var (Lvec)
#Lorb = self.base.unpack_uniq_var (Lvec[:self.ngorb])
#Lci = Lvec[self.ngorb:].reshape (self.nroots, -1)
#ci = np.ravel (ci).reshape (self.nroots, -1)
# CI part
t0 = (logger.process_clock(), logger.perf_counter())
de_Lci = Lci_dot_dgci_dx(Lci, self.weights, self.base, mo_coeff=mo, ci=ci,
atmlst=atmlst, mf_grad=mf_grad, eris=eris, verbose=verbose)
logger.info (self, '--------------- %s gradient Lagrange CI response ---------------',
self.base.__class__.__name__)
if verbose >= logger.INFO: rhf_grad._write(self, self.mol, de_Lci, atmlst)
logger.info (self, '----------------------------------------------------------------')
t0 = logger.timer (self, '{} gradient Lagrange CI response'.format (
self.base.__class__.__name__), *t0)
# Orb part
de_Lorb = Lorb_dot_dgorb_dx(Lorb, self.base, mo_coeff=mo, ci=ci,
atmlst=atmlst, mf_grad=mf_grad, eris=eris, verbose=verbose)
logger.info (self, '--------------- %s gradient Lagrange orbital response ---------------',
self.base.__class__.__name__)
if verbose >= logger.INFO: rhf_grad._write(self, self.mol, de_Lorb, atmlst)
logger.info (self, '---------------------------------------------------------------------')
t0 = logger.timer (self, '{} gradient Lagrange orbital response'.format (
self.base.__class__.__name__), *t0)
return de_Lci + de_Lorb
[docs]
def debug_lagrange (self, Lvec, bvec, Aop, Adiag, state=None, mo=None, ci=None, **kwargs):
# This needs to be rewritten substantially to work properly with state_average_mix
if state is None: state = self.state
if mo is None: mo = self.base.mo_coeff
if ci is None: ci = self.base.ci
def _debug_cispace (xci, 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:
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]
for ix, (norm, ss, multip) in enumerate (zip (xci_norm, xci_ss, xci_multip)):
logger.debug (self,
' State {} {} norm = {:.7e} ; <S^2> = {:.7f} ; 2S+1 = {:.7f}'.format(
ix, label, norm, ss, multip))
borb, bci = self.unpack_uniq_var (bvec)
logger.debug (self, 'Orbital rotation gradient norm = {:.7e}'.format (linalg.norm (borb)))
_debug_cispace (bci, 'CI gradient')
Aorb, Aci = self.unpack_uniq_var (Adiag)
logger.debug (self, 'Orbital rotation Hamiltonian diagonal norm = {:.7e}'.format (
linalg.norm (Aorb)))
_debug_cispace (Aci, 'Hamiltonian diagonal')
Lorb, Lci = self.unpack_uniq_var (Lvec)
logger.debug (self, 'Orbital rotation Lagrange vector norm = {:.7e}'.format (
linalg.norm (Lorb)))
_debug_cispace (Lci, 'Lagrange vector')
[docs]
def get_lagrange_precond (self, Adiag, level_shift=None, ci=None, **kwargs):
if level_shift is None: level_shift = self.level_shift
if ci is None: ci = self.base.ci
return SACASLagPrec (Adiag=Adiag, level_shift=level_shift, ci=ci, grad_method=self)
[docs]
def get_lagrange_callback (self, Lvec_last, itvec, geff_op):
def my_call (x):
itvec[0] += 1
geff = geff_op (x)
deltax = x - Lvec_last
gorb, gci = self.unpack_uniq_var (geff)
deltaorb, deltaci = self.unpack_uniq_var (deltax)
gci = np.concatenate ([g.ravel () for g in gci])
deltaci = np.concatenate ([d.ravel () for d in deltaci])
logger.info(self, ('Lagrange optimization iteration {}, |gorb| = {}, |gci| = {}, '
'|dLorb| = {}, |dLci| = {}').format (
itvec[0], linalg.norm (gorb), linalg.norm (gci),
linalg.norm (deltaorb), linalg.norm (deltaci)))
Lvec_last[:] = x[:]
return my_call
[docs]
def project_Aop (self, Aop, ci, state):
''' Wrap the Aop function to project out redundant degrees of freedom for the CI part.
What's redundant changes between SA-CASSCF and MC-PDFT so modify this part in child
classes. '''
def my_Aop (x):
Ax = Aop (x)
Ax_orb, Ax_ci = self.unpack_uniq_var (Ax)
for i, j in product (range (self.nroots), repeat=2):
# I'm assuming the only symmetry here that's actually built into the data structure
# is solver.spin. This will be the case as long as the various solvers are
# determinants with a common total charge occupying a common set of orbitals
if self.spin_states[i] != self.spin_states[j]: continue
Ax_ci[i] -= np.dot (Ax_ci[i].ravel (), ci[j].ravel ()) * ci[j]
#Ax_ci = Ax[self.ngorb:].reshape (self.nroots, -1)
#ci_arr = np.asarray (ci).reshape (self.nroots, -1)
#ovlp = np.dot (ci_arr.conjugate (), Ax_ci.T)
#Ax_ci -= np.dot (ovlp.T, ci_arr)
#Ax[self.ngorb:] = Ax_ci.ravel ()
return self.pack_uniq_var (Ax_orb, Ax_ci)
return my_Aop
as_scanner = as_scanner
[docs]
class SACASLagPrec (lagrange.LagPrec):
''' A callable preconditioner for solving the Lagrange equations.
Based on Mol. Phys. 99, 103 (2001).
Attributes:
nroots : integer
Number of roots in the SA space
nlag : integer
Number of Lagrange degrees of freedom
ngorb : integer
Number of Lagrange degrees of freedom which are orbital rotations
level_shift : float
numerical shift applied to CI rotation Hessian
ci : ndarray of shape (nroots, ndet or ncscf)
Ci vectors of the SA space
Rorb : ndarray of shape (ngorb)
Diagonal inverse Hessian matrix for orbital rotations
Rci : ndarray of shape (nroots, ndet or ncsf)
Diagonal inverse Hessian matrix for CI rotations including a level shift
Rci_sa : ndarray of shape (nroots (I), ndet or ncsf, nroots (K))
First two factors of the inverse diagonal CI Hessian projected into SA space:
Rci(I)|J> <J|Rci(I)|K>^{-1} <K|Rci(I)
note: right-hand bra and R_I factor not included due to storage considerations
Make the operand's matrix element with <K|Rci(I) before taking the dot product!
'''
_keys = {
'level_shift', 'nroots', 'nlag', 'ngorb', 'spin_states', 'na_states',
'nb_states', 'grad_method', 'Rorb', 'ci', 'Rci', 'Rci_sa',
}
# TODO: fix me (subclass me? wrap me?) for state_average_mix
def __init__(self, Adiag=None, level_shift=None, ci=None, grad_method=None):
self.level_shift = level_shift
self.nroots = grad_method.nroots
self.nlag = grad_method.nlag
self.ngorb = grad_method.ngorb
self.spin_states = grad_method.spin_states
self.na_states = grad_method.na_states
self.nb_states = grad_method.nb_states
self.grad_method = grad_method
Aorb, Aci = self.unpack_uniq_var (Adiag)
self._init_orb (Aorb)
self._init_ci (Aci, ci)
[docs]
def unpack_uniq_var (self, x):
return self.grad_method.unpack_uniq_var (x)
[docs]
def pack_uniq_var (self, xorb, xci):
return self.grad_method.pack_uniq_var (xorb, xci)
def _init_orb (self, Aorb):
self.Rorb = Aorb
self.Rorb[abs(self.Rorb)<1e-8] = 1e-8
self.Rorb = 1./self.Rorb
def _init_ci (self, Aci_spins, ci_spins):
self.ci = []
self.Rci = []
self.Rci_sa = []
for [Aci, ci] in self._iterate_ci (Aci_spins, ci_spins):
nroots = Aci.shape[0]
Rci = Aci + self.level_shift
Rci[abs(Rci)<1e-8] = 1e-8
Rci = 1./Rci
# R_I|J>
# Indices: I, det, J
Rci_cross = Rci[:,:,None] * ci.T[None,:,:]
# S(I)_JK = <J|R_I|K> (first index of CI contract with middle index of R_I|J>)
# and reshape to put I first
Sci = np.tensordot (ci.conjugate (), Rci_cross, axes=(1,1)).transpose (1,0,2)
# R_I|J> S(I)_JK^-1 (can only loop explicitly because of necessary call to linalg.inv)
# Indices: I, det, K
Rci_sa = np.zeros_like (Rci_cross)
for iroot in range (nroots):
Rci_sa[iroot] = np.dot (Rci_cross[iroot], linalg.inv (Sci[iroot]))
self.ci.append (ci)
self.Rci.append (Rci)
self.Rci_sa.append (Rci_sa)
def _iterate_ci (self, *args):
# All args must be iterables over CI vectors in input order
# Eventually, get rid of copying (np.asarray, etc.)
# Don't assume args are ndarrays on input
for my_spin in np.unique (self.spin_states):
idx = np.where (self.spin_states == my_spin)[0]
yield [np.asarray ([arg[i] for i in idx]).reshape (len (idx), -1) for arg in args]
def __call__(self, x):
xorb, xci = self.unpack_uniq_var (x)
Mxorb = self.orb_prec (xorb)
Mxci = self.ci_prec (xci)
return self.pack_uniq_var (Mxorb, Mxci)
[docs]
def orb_prec (self, xorb):
return self.Rorb * xorb
[docs]
def ci_prec (self, xci_spins):
Mxci = [None,] * self.nroots
for ix_spin, [xci, desort_spin] in enumerate (
self._iterate_ci (xci_spins, list(range(self.nroots)))):
desort_spin = np.atleast_1d (np.squeeze (desort_spin))
nroots = xci.shape[0]
ci = self.ci[ix_spin]
Rci = self.Rci[ix_spin]
Rci_sa = self.Rci_sa[ix_spin]
# R_I|H I> (indices: I, det)
Rx = Rci * xci
# <J|R_I|H I> (indices: J, I)
sa_ovlp = np.dot (ci.conjugate (), Rx.T)
# R_I|J> S(I)_JK^-1 <K|R_I|H I> (indices: I, det)
Rx_sub = np.zeros_like (Rx)
for iroot in range (nroots):
Rx_sub[iroot] = np.dot (Rci_sa[iroot], sa_ovlp[:,iroot])
for i, j in enumerate (desort_spin):
try:
Mxci[j] = Rx[i] - Rx_sub[i]
except Exception as e:
print (i, j, desort_spin)
raise (e)
assert (all (i is not None for i in Mxci))
return Mxci
mcscf.addons.StateAverageMCSCFSolver.Gradients = lib.class_as_method(Gradients)