Source code for pyscf.grad.lpdft

#!/usr/bin/env python
# Copyright 2014-2023 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.
#
# Author: Matthew Hennefarth <mhennefarth@uchicago.com>

from pyscf.grad import rks as rks_grad
from pyscf.dft import gen_grid
from pyscf.lib import logger, tag_array, pack_tril, current_memory
from pyscf.mcscf import casci, mc1step, newton_casscf
from pyscf.grad import sacasscf
from pyscf.mcscf.casci import cas_natorb

from pyscf.mcpdft.otpd import get_ontop_pair_density, _grid_ao2mo
from pyscf.mcpdft.tfnal_derivs import contract_fot, unpack_vot, contract_vot
from pyscf.mcpdft import _dms
from pyscf.mcpdft import mspdft
import pyscf.grad.mcpdft as mcpdft_grad

import numpy as np
import gc

BLKSIZE = gen_grid.BLKSIZE


[docs] def get_ontop_response( mc, ot, state, atmlst, casdm1, casdm1_0, mo_coeff=None, ci=None, max_memory=None ): if mo_coeff is None: mo_coeff = mc.mo_coeff if ci is None: ci = mc.ci if max_memory is None: max_memory = mc.max_memory t0 = (logger.process_clock(), logger.perf_counter()) mol = mc.mol ncore = mc.ncore ncas = mc.ncas nocc = ncore + ncas nao, nmo = mo_coeff.shape dvxc = np.zeros((3, nao)) de_grid = np.zeros((len(atmlst), 3)) de_wgt = np.zeros((len(atmlst), 3)) mo_coeff_0, ci_0, mo_occup_0 = cas_natorb( mc, mo_coeff=mo_coeff, ci=ci, casdm1=casdm1_0 ) mo_coeff, ci, mo_occup = cas_natorb(mc, mo_coeff=mo_coeff, ci=ci, casdm1=casdm1) mo_occ = mo_coeff[:, :nocc] mo_cas = mo_coeff[:, ncore:nocc] mo_occ_0 = mo_coeff_0[:, :nocc] mo_cas_0 = mo_coeff_0[:, ncore:nocc] # Need to regenerate these with the updated ci values.... casdm1s = mc.make_one_casdm1s(ci=ci, state=state) casdm1 = casdm1s[0] + casdm1s[1] casdm2 = mc.make_one_casdm2(ci=ci, state=state) dm1s = _dms.casdm1s_to_dm1s(mc, casdm1s, mo_coeff=mo_coeff, ncore=ncore, ncas=ncas) dm1 = dm1s[0] + dm1s[1] dm1 = tag_array(dm1, mo_coeff=mo_coeff[:, :nocc], mo_occ=mo_occup[:nocc]) casdm1s_0, casdm2_0 = mc.get_casdm12_0(ci=ci_0) casdm1_0 = casdm1s_0[0] + casdm1s_0[1] dm1s_0 = _dms.casdm1s_to_dm1s( mc, casdm1s_0, mo_coeff=mo_coeff_0, ncore=ncore, ncas=ncas ) dm1_0 = dm1s_0[0] + dm1s_0[1] dm1_0 = tag_array(dm1_0, mo_coeff=mo_coeff_0[:, :nocc], mo_occ=mo_occup_0[:nocc]) cascm2 = _dms.dm2_cumulant(casdm2, casdm1) cascm2_0 = _dms.dm2_cumulant(casdm2_0, casdm1_0) make_rho = ot._numint._gen_rho_evaluator(mol, dm1, 1)[0] make_rho_0 = ot._numint._gen_rho_evaluator(mol, dm1_0, 1)[0] idx = np.array([[1, 4, 5, 6], [2, 5, 7, 8], [3, 6, 8, 9]], dtype=np.int_) # For addressing particular ao derivatives if ot.xctype == "LDA": idx = idx[:, 0:1] # For LDAs, no second derivatives casdm2_pack = mcpdft_grad.pack_casdm2(cascm2, ncas) casdm2_0_pack = mcpdft_grad.pack_casdm2(cascm2_0, ncas) full_atmlst = -np.ones(mol.natm, dtype=np.int_) for k, ia in enumerate(atmlst): full_atmlst[ia] = k t1 = logger.timer(mc, "L-PDFT HlFn quadrature setup", *t0) ndao = (1, 4)[ot.dens_deriv] ndpi = (1, 4)[ot.Pi_deriv] ncols = 1.05 * 3 * (ndao * nao + nocc) + max(ndao * nao, ndpi * ncas * ncas) # I have no idea if this is actually the correct number of columns, but I have a feeling it is not since I should # be accounting for the extra rows from feff stuff... for ia, (coords, w0, w1) in enumerate(rks_grad.grids_response_cc(ot.grids)): gc.collect() ngrids = coords.shape[0] remaining_floats = (max_memory - current_memory()[0]) * 1e6 / 8 blksize = int(remaining_floats / (ncols * BLKSIZE)) * BLKSIZE blksize = max(BLKSIZE, min(blksize, ngrids, BLKSIZE * 1200)) t1 = logger.timer( mc, "L-PDFT HlFn quadrature atom {} mask and memory setup".format(ia), *t1 ) for ip0 in range(0, ngrids, blksize): ip1 = min(ngrids, ip0 + blksize) mask = gen_grid.make_mask(mol, coords[ip0:ip1]) logger.info( mc, "L-PDFT gradient atom {} slice {}-{} of {} total".format( ia, ip0, ip1, ngrids ), ) ao = ot._numint.eval_ao( mol, coords[ip0:ip1], deriv=ot.dens_deriv + 1, non0tab=mask ) t1 = logger.timer( mc, "L-PDFT HlFn quadrature atom {} ao grids".format(ia), *t1 ) if ot.xctype == "LDA": aoval = ao[0] if ot.xctype == "GGA": aoval = ao[:4] rho = make_rho(0, aoval, mask, ot.xctype) / 2.0 rho = np.stack((rho,) * 2, axis=0) rho_0 = make_rho_0(0, aoval, mask, ot.xctype) / 2.0 rho_0 = np.stack((rho_0,) * 2, axis=0) delta_rho = rho - rho_0 t1 = logger.timer( mc, "L-PDFT HlFn quadrature atom {} rho calc".format(ia), *t1 ) Pi = get_ontop_pair_density( ot, rho, aoval, cascm2, mo_cas, ot.dens_deriv, mask ) Pi_0 = get_ontop_pair_density( ot, rho_0, aoval, cascm2_0, mo_cas_0, ot.dens_deriv, mask ) delta_Pi = Pi - Pi_0 t1 = logger.timer( mc, "L-PDFT HlFn quadrature atom {} Pi calc".format(ia), *t1 ) if ot.xctype == "LDA": aoval = ao[:1] moval_occ = _grid_ao2mo(mol, aoval, mo_occ, mask) moval_occ_0 = _grid_ao2mo(mol, aoval, mo_occ_0, mask) t1 = logger.timer( mc, "L-PDFT HlFn quadrature atom {} ao2mo grids".format(ia), *t1 ) aoval = np.ascontiguousarray( [ao[ix].transpose(0, 2, 1) for ix in idx[:, :ndao]] ).transpose(0, 1, 3, 2) ao = None t1 = logger.timer( mc, "L-PDFT HlFn quadrature atom {} ao grid reshape".format(ia), *t1 ) eot, vot, fot = ot.eval_ot( rho_0, Pi_0, weights=w0[ip0:ip1], dderiv=2, _unpack_vot=False ) fot = contract_fot( ot, fot, rho_0, Pi_0, delta_rho, delta_Pi, unpack=True, vot_packed=vot ) vot = unpack_vot(vot, rho_0, Pi_0) # See the equations... eot += contract_vot(vot, delta_rho, delta_Pi) t1 = logger.timer( mc, "PDFT HlFn quadrature atom {} eval_ot".format(ia), *t1 ) puvx_mem = 2 * ndpi * (ip1 - ip0) * ncas * ncas * 8 / 1e6 remaining_mem = max_memory - current_memory()[0] logger.info( mc, ( "L-PDFT gradient memory note: working on {} grid points: estimated puvx usage = {:.1f} of {:.1f} " "remaining MB" ).format((ip1 - ip0), puvx_mem, remaining_mem), ) # Weight response de_wgt += np.tensordot(eot, w1[atmlst, ..., ip0:ip1], axes=(0, 2)) t1 = logger.timer( mc, "L-PDFT HlFn quadrature atom {} weight response".format(ia), *t1 ) # The mo_occup values might be screwing me here... # rho_delta * fot * drho_SA/dx tmp_df = mcpdft_grad.xc_response( ot, fot, rho_0, Pi_0, w0[ip0:ip1], moval_occ_0, aoval, mo_occ_0, mo_occup_0, ncore, nocc, casdm2_0_pack, ndpi, mo_cas_0, ) # vot * drho_Gamma/dx tmp_dv = mcpdft_grad.xc_response( ot, vot, rho, Pi, w0[ip0:ip1], moval_occ, aoval, mo_occ, mo_occup, ncore, nocc, casdm2_pack, ndpi, mo_cas, ) tmp_dxc = tmp_df + tmp_dv # Find the atoms that are part of the atomlist # grid correction shouldn't be added if they arent there k = full_atmlst[ia] if k >= 0: de_grid[k] += 2 * tmp_dxc.sum(1) # Grid response dvxc -= tmp_dxc # XC response tmp_dxc = tmp_df = tmp_dv = None t1 = logger.timer(mc, "L-PDFT HlFn quadrature atom {}".format(ia), *t1) rho_0 = Pi_0 = rho = Pi = delta_rho = delta_Pi = None eot = vot = fot = aoval = moval_occ = moval_occ_0 = None gc.collect() return dvxc, de_wgt, de_grid
[docs] def lpdft_HellmanFeynman_grad( mc, ot, state, feff1, feff2, mo_coeff=None, ci=None, atmlst=None, mf_grad=None, verbose=None, max_memory=None, auxbasis_response=False, ): 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.get_rhf_base ().nuc_grad_method() if mc.frozen is not None: raise NotImplementedError mol = mc.mol if atmlst is None: atmlst = range(mol.natm) t0 = (logger.process_clock(), logger.perf_counter()) # Specific state density casdm1s = mc.make_one_casdm1s(ci=ci, state=state) casdm1 = casdm1s[0] + casdm1s[1] dm1s = _dms.casdm1s_to_dm1s(mc, casdm1s=casdm1s, mo_coeff=mo_coeff) dm1 = dm1s[0] + dm1s[1] casdm2 = mc.make_one_casdm2(ci=ci, state=state) # The model-space density (or state-average density) casdm1s_0, casdm2_0 = mc.get_casdm12_0() dm1s_0 = _dms.casdm1s_to_dm1s(mc, casdm1s=casdm1s_0, mo_coeff=mo_coeff) dm1_0 = dm1s_0[0] + dm1s_0[1] casdm1_0 = casdm1s_0[0] + casdm1s_0[1] # Generate the Generalized Fock Component gfock_expl = mcpdft_grad.gfock_sym( mc, mo_coeff, casdm1, casdm2, mc.get_lpdft_hcore(), mc.veff2 ) gfock_impl = mcpdft_grad.gfock_sym(mc, mo_coeff, casdm1_0, casdm2_0, feff1, feff2) gfock = gfock_expl + gfock_impl dme0 = mo_coeff @ (0.5 * (gfock + gfock.T)) @ mo_coeff.T del gfock, gfock_impl, gfock_expl t0 = logger.timer(mc, "L-PDFT HlFn gfock", *t0) # Coulomb potential derivatives generated from zero-order density dvj_all = mf_grad.get_j(dm=[dm1, dm1_0]) dvj = dvj_all[0] dvj_0 = dvj_all[1] dvxc, de_wgt, de_grid = get_ontop_response( mc, ot, state, atmlst, casdm1, casdm1_0, mo_coeff=mo_coeff.copy(), ci=ci.copy(), max_memory=max_memory, ) delta_dm1 = dm1 - dm1_0 def coul_term(p0, p1): return 2 * ( np.tensordot(dvj_0[:, p0:p1], delta_dm1[p0:p1]) + np.tensordot(dvj[:, p0:p1], dm1_0[p0:p1]) ) de_hcore, de_coul, de_xc, de_nuc, de_renorm = mcpdft_grad.sum_terms( mf_grad, mol, atmlst, dm1, dme0, coul_term, dvxc ) logger.debug(mc, "L-PDFT Hellmann-Feynman nuclear:\n{}".format(de_nuc)) logger.debug(mc, "L-PDFT Hellmann-Feynman hcore component:\n{}".format(de_hcore)) logger.debug(mc, "L-PDFT Hellmann-Feynman coulomb component:\n{}".format(de_coul)) logger.debug(mc, "L-PDFT Hellmann-Feynman xc component:\n{}".format(de_xc)) logger.debug( mc, "L-PDFT Hellmann-Feynman quadrature point component:\n{}".format(de_grid) ) logger.debug( mc, "L-PDFT Hellmann-Feynman quadrature weight component:\n{}".format(de_wgt) ) logger.debug(mc, "L-PDFT Hellmann-Feynman renorm component:\n{}".format(de_renorm)) de = de_nuc + de_hcore + de_coul + de_renorm + de_xc + de_grid + de_wgt if auxbasis_response: dvj_aux = dvj_all.aux[:,:,atmlst,:] de_aux = dvj_aux[1, 0] + dvj_aux[0, 1] - dvj_aux[1, 1] logger.debug(mc, "L-PDFT Hellmann-Feynman aux component:\n{}".format(de_aux)) de += de_aux logger.timer(mc, "L-PDFT HlFn total", *t0) return de
[docs] class Gradients(sacasscf.Gradients): def __init(self, pdft, state=None): super().__init__(pdft, state=state) if self.state is None and self.nroots == 1: self.state = 0 self.e_mcscf = self.base.e_mcscf self._not_implemented_check() def _not_implemented_check(self): name = self.__class__.__name__ if isinstance(self.base, casci.CASCI) and not isinstance( self.base, mc1step.CASSCF ): raise NotImplementedError("{} for CASCI-based MC-PDFT".format(name)) ot, otxc, nelecas = self.base.otfnal, self.base.otxc, self.base.nelecas spin = abs(nelecas[0] - nelecas[1]) omega, alpha, hyb = ot._numint.rsh_and_hybrid_coeff(otxc, spin=spin) hyb_x, hyb_c = hyb if hyb_x or hyb_c: raise NotImplementedError("{} for hybrid MC-PDFT functionals".format(name)) if omega: raise NotImplementedError( "{} for range-separated MC-PDFT functionals".format(name) )
[docs] def kernel(self, **kwargs): state = kwargs["state"] if "state" in kwargs else self.state if state is None: raise NotImplementedError("Gradient of LPDFT state-average energy") self.state = state mo = kwargs["mo"] if "mo" in kwargs else self.base.mo_coeff ci = kwargs["ci"] if "ci" in kwargs else self.base.ci if isinstance(ci, np.ndarray): ci = [ci] # hack hack hack????? idk kwargs["ci"] = ci # need to compute feff1, feff2 if not already in kwargs if ("feff1" not in kwargs) or ("feff2" not in kwargs): kwargs["feff1"], kwargs["feff2"] = self.get_otp_gradient_response( mo, ci, state ) return super().kernel(**kwargs)
[docs] def get_wfn_response( self, state=None, verbose=None, mo=None, ci=None, feff1=None, feff2=None, **kwargs ): """Returns the derivative of the L-PDFT energy for the given state with respect to MO parameters and CI parameters. Care is take to account for the implicit and explicit response terms, and make sure the CI vectors are properly projected out. Args: state : int Which state energy to get response of. mo : ndarray of shape (nao, nmo) A full set of molecular orbital coefficients. Taken from self if not provided. ci : list of ndarrays of length nroots CI vectors should be from a converged L-PDFT calculation. feff1 : ndarray of shape (nao, nao) 1-particle On-top gradient response which as been contracted with the Delta density generated from state. Should include the Coulomb term as well. feff2 : pyscf.mcscf.mc_ao2mo._ERIS instance Relevant 2-body on-top gradient response terms in the MO basis. Also, partially contracted with the Delta density. Returns: g_all : ndarray of shape nlag First sector [:self.ngorb] contains the derivatives with respect to MO parameters. Second sector [self.ngorb:] contains the derivatives with respect to CI parameters. """ if state is None: state = self.state 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 verbose is None: verbose = self.verbose if (feff1 is None) or (feff2 is None): feff1, feff2 = self.get_otp_gradient_response(mo, ci, state) log = logger.new_logger(self, verbose) ndet = self.na_states[state] * self.nb_states[state] fcasscf = self.make_fcasscf(state) # Exploit (hopefully) the fact that the zero-order density is # really just the State Average Density! fcasscf_sa = self.make_fcasscf_sa() fcasscf.mo_coeff = mo fcasscf.ci = ci[state] fcasscf.get_hcore = self.base.get_lpdft_hcore fcasscf_sa.get_hcore = lambda: feff1 g_all_explicit = newton_casscf.gen_g_hop( fcasscf, mo, ci[state], self.base.veff2, verbose )[0] g_all_implicit = newton_casscf.gen_g_hop(fcasscf_sa, mo, ci, feff2, verbose)[0] # Debug log.debug("g_all explicit orb:\n{}".format(g_all_explicit[: self.ngorb])) log.debug("g_all explicit ci:\n{}".format(g_all_explicit[self.ngorb :])) log.debug("g_all implicit orb:\n{}".format(g_all_implicit[: self.ngorb])) log.debug("g_all implicit ci:\n{}".format(g_all_implicit[self.ngorb :])) # Need to remove the SA-SA rotations from g_all_implicit CI contributions spin_states = np.asarray(self.spin_states) gmo_implicit, gci_implicit = self.unpack_uniq_var(g_all_implicit) for root in range(self.nroots): idx_spin = spin_states == spin_states[root] idx = np.where(idx_spin)[0] gci_root = gci_implicit[root].ravel() assert root in idx ci_proj = np.asarray([ci[i].ravel() for i in idx]) gci_sa = np.dot(ci_proj, gci_root) gci_root -= np.dot(gci_sa, ci_proj) gci_implicit[root] = gci_root g_all = self.pack_uniq_var(gmo_implicit, gci_implicit) g_all[: self.ngorb] += g_all_explicit[: self.ngorb] offs = ( sum( [ na * nb for na, nb in zip(self.na_states[:state], self.nb_states[:state]) ] ) if root > 0 else 0 ) g_all[self.ngorb :][offs:][:ndet] += g_all_explicit[self.ngorb :] gorb, gci = self.unpack_uniq_var(g_all) log.debug("g_all orb:\n{}".format(gorb)) log.debug("g_all ci:\n{}".format([c.ravel() for c in gci])) return g_all
[docs] def get_ham_response( self, state=None, atmlst=None, verbose=None, mo=None, ci=None, mf_grad=None, feff1=None, feff2=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 (feff1 is None) or (feff2 is None): assert False, kwargs return lpdft_HellmanFeynman_grad( self.base, self.base.otfnal, state, feff1=feff1, feff2=feff2, mo_coeff=mo, ci=ci, atmlst=atmlst, mf_grad=mf_grad, verbose=verbose, )
[docs] def get_otp_gradient_response(self, mo=None, ci=None, state=0): """Generate the 1- and 2-body on-top gradient response terms which have been partially contracted with the Delta density generated from state. Args: mo : ndarray of shape (nao,nmo) A full set of molecular orbital coefficients. Taken from self if not provided. ci : list of ndarrays of length nroots CI vectors should be from a converged L-PDFT calculation state : int State to generate the Delta density with Returns: feff1 : ndarray of shape (nao, nao) 1-particle On-top gradient response which as been contracted with the Delta density generated from state. Should include the Coulomb term as well. feff2 : pyscf.mcscf.mc_ao2mo._ERIS instance Relevant 2-body on-top gradient response terms in the MO basis. Also, partially contracted with the Delta density. """ if mo is None: mo = self.base.mo_coeff if ci is None: ci = self.base.ci if state is None: state = self.state # This is the zero-order density casdm1s_0, casdm2_0 = self.base.get_casdm12_0() # This is the density of the state we are differentiating with respect to casdm1s = self.base.make_one_casdm1s(ci=ci, state=state) casdm2 = self.base.make_one_casdm2(ci=ci, state=state) dm1s = _dms.casdm1s_to_dm1s(self.base, casdm1s, mo_coeff=mo) cascm2 = _dms.dm2_cumulant(casdm2, casdm1s) return self.base.get_pdft_feff( mo=mo, ci=ci, casdm1s=casdm1s_0, casdm2=casdm2_0, c_dm1s=dm1s, c_cascm2=cascm2, jk_pc=True, paaa_only=True, incl_coul=True, delta=True, )
[docs] def get_Aop_Adiag( self, verbose=None, mo=None, ci=None, eris=None, state=None, **kwargs ): """This function accounts for the fact that the CI vectors are no longer eigenstates of the CAS Hamiltonian. It adds back in the necessary values to the Hessian.""" 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 state is None: state = self.state if eris is None and self.eris is None: eris = self.eris = self.base.ao2mo(mo) elif eris is None: eris = self.eris ham_od = mspdft.make_heff_mcscf(self.base, mo_coeff=mo, ci=ci) fcasscf = self.make_fcasscf_sa() hop, Adiag = newton_casscf.gen_g_hop(fcasscf, mo, ci, eris, verbose)[2:] # TODO: (MR Hennefarth) This is not safe way to check if something is a # mixed solver, can probably do better than this... if hasattr(self.base, "_irrep_slices"): for ham_slice in ham_od: ham_slice[np.diag_indices_from(ham_slice)] = 0.0 ham_slice += ( ham_slice.T ) # This corresponds to the arbitrary newton_casscf*2 def Aop(x): Ax = hop(x) x_c = self.unpack_uniq_var(x)[1] Ax_o, Ax_c = self.unpack_uniq_var(Ax) for irrep_slice, ham_slice in zip(self.base._irrep_slices, ham_od): Ax_c_od_slice = list( np.tensordot( -ham_slice, np.stack(x_c[irrep_slice], axis=0), axes=1 ) ) Ax_c[irrep_slice] = [ a1 + (w * a2) for a1, a2, w in zip( Ax_c[irrep_slice], Ax_c_od_slice, self.base.weights[irrep_slice], ) ] return self.pack_uniq_var(Ax_o, Ax_c) else: ham_od[np.diag_indices_from(ham_od)] = 0.0 ham_od += ham_od.T # This corresponds to the arbitrary newton_casscf*2 def Aop(x): Ax = hop(x) x_c = self.unpack_uniq_var(x)[1] Ax_o, Ax_c = self.unpack_uniq_var(Ax) 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) return self.project_Aop(Aop, ci, state), Adiag