Closed matthew-hennefarth closed 1 year ago
For testing
pdft_veff
, I generally understand what is happening here. Thegen_g_hop
gets the gradient, gradient update, hessian_update, and hessian_diag elements, right? Does g_all, in the orbital sector (ieg_all[:ngorb]
) contain the Fxy−Fyx where Fxy is the generalized Fock matrix elements given theeris
andget_hcore()
in the current environment?
Yes. Furthermore, the remaining elements (g_all[ngorb:]
) are gradients of the energy with respect to CI vector relaxations towards individual determinants:
$$\langle i|\hat{H}-E_0|0\rangle + \mathrm{h.c.}$$
Regardless, you get the
x0
term which is the step in some direction. You then scale that term progressively smaller and checking to see if you get numerical convergence of the difference between the analytical and numerical derivative (summary of these lines). I am a bit confused on whatnp.dot(g_all, x1)
represents here mathematically (which is conflated with what exactlyg_all
is).
The product of a gradient with a step vector is an approximation to the energy difference after that step, which becomes exact in the limit of small step size with a definite constant of proportionality:
$$ \Delta f = f(1)\delta + \frac{1}{2}f(2)\delta^2 + \cdots $$
$$ \lim_{\delta\to 0} \frac{\Delta f - f(1)\delta}{\Delta f} = \frac{1}{2} \frac{f(2)}{f(1)}\delta $$
In other words, there is an asymptotic plateau where if you halve the step size, you should also halve the relative error of the linear approximation. np.dot (g_all, x1)
is $f(1)\delta$ and seminum (x1)
is $\Delta f$. The version of this for second derivatives is
$$ \Delta (f') = f(2)\delta + \frac{1}{2}f(3)\delta^2 + \cdots $$
$$ \lim_{\delta\to 0} \frac{\Delta (f') - f(2)\delta}{\Delta (f')} = \frac{1}{2} \frac{f(3)}{f(2)}\delta $$
but now the left and right-hand sides of the expression are both vectors. I compare them by evaluating the norm of the difference vector here.
With that being said, how should I go about testing the
pdft_feff
terms numerically? I understand that you wrote theEotOrbitalHessianOperator
and test this, but is there a simpler way of testing without having to copy a bunch of code from the file? You also discuss needing to use a "dressedgen_g_hop
" object (here).
I'm sure there is a simpler way to implement because my implementation is incredible spaghetti. But regarding the "dressed gen_g_hop
" comment, that is a reminder that what you call the "explicit" contribution to the Hessian is mappable to a CASSCF gradient with a modified Hamiltonian. That's what that entire conditional block of the initializer is about: if incl_d2rho==False
, then only the terms in the Hessian-vector product which involve $\mathbf{f}^\mathrm{ot}$ are computed.
Lastly, I am a bit confused on what exactly
paaa_only
does for the_ERIS
(see these lines). If I understand correctly, you are adding the active space contribution of the HF potential to the HF potential core term? I know this is important since similar terms appear in thegen_g_hop
code and theEotOrbitalHessianOperator
vhf_c
by default is
$$ V{pq} = v{pqii}Dii - \frac{1}{2}v{piiq}Dii = \frac{1}{2}v{pqii}D{ii} = v{pqii} = \int v{\Pi} \rho{\mathrm{core}} \frac{\partial\rho}{\partial D_{pq}} $$
If paaa_only
, I hijack it to add
$$ V{ai} \mathrel{+}= v{aiuv}D{uv} - \frac{1}{2}v{avui} D{uv} = \frac{1}{2}v{aiuv}D{uv} = \int v{\Pi} \rho{\mathrm{active}} \frac{\partial\rho}{\partial D{ai}} $$
$$ V{aa} \mathrel{+}= v{aauv}D{uv} \cdots = \int v{\Pi} \rho{\mathrm{active}} \frac{\partial\rho}{\partial D{aa}} $$
$$ V{ii} \mathrel{+}= v{iiuv}D{uv+} \cdots = \int v{\Pi} \rho{\mathrm{active}} \frac{\partial\rho}{\partial D{ii}} $$
$$ V{ia} \mathrel{+}= v{iauv}D{uv} \cdots = \int v{\Pi} \rho{\mathrm{active}} \frac{\partial\rho}{\partial D{ia}} $$
since these lead to contributions to the orbital gradient that, by default in PySCF, are computed using explicit two-electron interaction elements of the appropriate index patterns (i.e., of type ppaa
and papa
), which we have not computed if paaa_only
. I think this would actually be a quicker algorithm for doing CASSCF orbital gradients themselves, if CASSCF didn't already need ppaa
and papa
ERIs anyway for orbital Hessians. But anyway the whole thing is designed so that the gradient that pops out of the CASSCF gen_g_hop
code is correct regardless of whether or not paaa_only
. But if you want Hessians, you'll need paaa_only=False
because you'll need ppaa
and papa
.
P.S. A bunch of factors of 1/2 are missing in the above five lines; don't take them too seriously. I'm just trying to show the orbital index patterns.
Okay, I think I understand a bit better, especially since I got it to sort of agree. What I have then is that $$\frac{\partial( V{pq}D{pq} + \frac{1}{2}v{pqrs}d{pqrs})}{\kappa{xy}} = F{xy} - F{yx}$$ where $$F{xy} = V{px}D{py} + v{pqrx}d{pqry} + [\rho\cdot \mathbf{F}]_{px}D_{py} + [\rho\cdot \mathbf{F}]_{pqrx}d_{pqry}$$ The "explicit" and "implicit" terms. What I can do then, is compute analytically this derivative by doing
feff1, feff2 = mc.get_pdft_feff(mc.mo_coeff, mc.ci, paaa_only=True)
veff1, veff2 = mc.get_pdft_veff(mc.mo_coeff, mc.ci, incl_coul=False, paaa_only=True)
with lib.temporary_env(fcasscf, get_hcore=lambda: feff1):
g_feff, _, _, hdiag_feff = newton_casscf.gen_g_hop(fcasscf, mc.mo_coeff, mc.ci, feff2)
with lib.temporary_env(fcasscf, get_hcore=lambda: veff1):
g_veff, _, _, hdiag_veff = newton_casscf.gen_g_hop(fcasscf, mc.mo_coeff, mc.ci, veff2)
I do it this way since I can't really add feff2
and veff2
since they are _ERIS
objects (and addition is not well defined). I then set g_all
and hdiag_all
as
g_all = g_feff + g_veff
hdiag_all = hdiag_feff + hdiag_veff
Though I am now unsure if I can just add hdiag_feff
and hdiag_veff
. Regardless, when I do this, I can then numerically differentiate
$$V{pq}D{pq} + \frac{1}{2}v{pqrs}d{pqrs} $$
by evaluating at the reference MO/CI and MO/CI + $\delta$, I get good agreement only when there are no core orbitals (ncore=0
, or just for H2). When I go to LiH, I no longer get agreement (which has ncore=1
).
I am doing it this way since feff1
and feff2
pop-out as already being partially contracted (ie they are your Hessian-vector products) and can't really be interrogated in any other way (at least that I can think of). You can see my progress here for testing.
Though I am now unsure if I can just add
hdiag_feff
andhdiag_veff
.
Just checked, and they agree with adding each term in the _ERIS
object. Still not sure why the numerical evaluation doesn't decrease by 1/2 when there are core orbitals.
On Friday we compared LiH (4e,3o) to LiH (2e,2o) and found that the differentiation fails for the latter but apparently not for the former. What happens if you take the LiH (2e,2o) wave function(s) and project it/them into the the LiH (4e,3o) Hilbert space? The cistring
module has functions for doing things like this, even though I'm pretty sure it's equivalent to just pad the CI vector with zeros:
import numpy as np
from pyscf.fci import cistring
from pyscf.fci.direct_spin1 import _unpack_nelec
ncore_add = 1 # core orbitals to put in the active space
nvirt_add = 0 # virtual orbitals to put in the active space
ci_small = mc.ci # It'll get annoying if you have more than one CI vector here
# throat-clearing
norb_small = mc.ncas
norb_large = norb_small + ncore_add + nvirt_add
neleca_small, nelecb_small = _unpack_nelec (mc.nelecas)
neleca_large = neleca_small + ncore_add
nelecb_large = nelecb_small + ncore_add
ndeta_large = cistring.num_strings (norb_large, neleca_large)
ndetb_large = cistring.num_strings (norb_large, nelecb_large)
ci_large = np.zeros ((ndeta_large, ndetb_large), dtype=ci_small.dtype)
# Find the addresses of all determinants with fully-occupied core electrons
# I don't trust the behavior of this function with a range of length 0
addrs_a_core = cistring.sub_addrs (norb_large, neleca_large, range (ncore_add), ncore_add) if ncore_add else []
addrs_b_core = cistring.sub_addrs (norb_large, nelecb_large, range (ncore_add), ncore_add) if ncore_add else []
# Find the addresses of all determinants with fully-unoccupied virtual electrons
nvirt_range = range (ncore_add+norb_small,norb_large)
addrs_a_virt = cistring.sub_addrs (norb_large, neleca_large, nvirt_range, 0) if nvirt_add else []
addrs_b_virt = cistring.sub_addrs (norb_large, nelecb_large, nvirt_range, 0) if nvirt_add else []
# The intersection of these two address spaces is the address space of the smaller active space in the larger one.
if ncore_add and nvirt_add:
addrs_a = np.intersect1d (addrs_a_core, addrs_a_virt)
addrs_b = np.intersect1d (addrs_b_core, addrs_b_virt)
elif ncore_add:
addrs_a, addrs_b = addrs_a_core, addrs_b_core
elif nvirt_add:
addrs_a, addrs_b = addrs_a_virt, addrs_b_virt
else:
raise RuntimeError ("whuddyadoin")
ci_large[np.ix_(addrs_a,addrs_b)] = ci_small[:,:]
Then all the f's and v's ought to be identical term-by-term.
Having looked at this myself, it looks like energy_core
might be the problem somehow:
/home/herme068/anaconda3/envs/pyscf/lib/python3.7/site-packages/pyscf/dft/libxc.py:772: UserWarning: Since PySCF-2.3, B3LYP (and B3P86) are changed to the VWN-RPA variant, corresponding to the original definition by Stephens et al. (issue 1480) and the same as the B3LYP functional in Gaussian. To restore the VWN5 definition, you can put the setting "B3LYP_WITH_VWN5 = True" in pyscf_conf.py
warnings.warn('Since PySCF-2.3, B3LYP (and B3P86) are changed to the VWN-RPA variant, '
Full Tests for pdft_feff
----------------------------------------------------------------------
mol = LiH state = Singlet fnal = ftLDA,VWN3 ncas = 2
eff1: -4.4845491423571815
aaaa (ncas=2): [[[[ 0.19993498 -0.03594766]
[-0.03594766 0.05524505]]
[[-0.03594766 0.05524505]
[ 0.05524505 -0.02883232]]]
[[[-0.03594766 0.05524505]
[ 0.05524505 -0.02883232]]
[[ 0.05524505 -0.02883232]
[-0.02883232 0.06511764]]]]
veff2.vhf_c (ncas=2): [[0.00374441 0.00157538]
[0.00157538 0.00132654]]
feff2.vhf_c (ncas=2): [[0.05546656 0.02278294]
[0.02278294 0.01373895]]
veff2.energy_core (ncas=2): 0.06549210880331385
feff2.energy_core (ncas=2): 1.1027722696928537
energy_core (ncas=2): 1.1682643784961675
MOs: 6, ncore: 1, ncas: 2, nelecas: (1, 1)
[0.5 0.40704163]
[0.5 0.32052317]
[0.5 0.14229989]
[0.5 0.02539863]
[ 0.5 14.22024932]
[0.5 1.25982536]
[0.5 1.0209301]
[0.5 0.98685039]
[0.5 0.98661162]
[0.5 0.99174401]
[0.5 0.99555796]
[0.5 0.99767472]
[0.5 0.99880896]
[0.5 0.99939692]
[0.5 0.99969651]
[0.5 0.99984776]
[0.5 0.99992375]
[0.5 0.99996185]
[0.5 0.99998093]
----------------------------------------------------------------------
mol = LiH state = Singlet fnal = ftLDA,VWN3 ncas = 3
eff1: -4.4845491423571815
aaaa (ncas=2): [[[[ 0.19993498 -0.03594766]
[-0.03594766 0.05524505]]
[[-0.03594766 0.05524505]
[ 0.05524505 -0.02883232]]]
[[[-0.03594766 0.05524505]
[ 0.05524505 -0.02883232]]
[[ 0.05524505 -0.02883232]
[-0.02883232 0.06511764]]]]
veff2.vhf_c (ncas=2): [[0.00374441 0.00157538]
[0.00157538 0.00132654]]
feff2.vhf_c (ncas=2): [[0.05546656 0.02278294]
[0.02278294 0.01373895]]
veff2.energy_core (ncas=2): 0.12722510880593546
feff2.energy_core (ncas=2): 2.1499647355000224
energy_core (ncas=2): 2.277189844305958
MOs: 6, ncore: 0, ncas: 3, nelecas: (2, 2)
[0.5 5.09490952]
[0.5 1.50077433]
[0.5 1.0446315]
[0.5 0.83870983]
[0.5 0.73245941]
[0.5 0.64315851]
[0.5 0.58123824]
[0.5 0.54346756]
[0.5 0.5223499]
[0.5 0.51135307]
[0.5 0.50575672]
[0.5 0.50290776]
[0.5 0.50146325]
[0.5 0.50073403]
[0.5 0.50036766]
.
----------------------------------------------------------------------
Ran 1 test in 1.542s
OK
For the ncas=3 calculation, I did math on the ppaa
array in the test script to generate the values that compare to the ncas=2 vhf_c
and energy_core
attributes. It worked perfectly for vhf_c
, but not for energy_core.
Note that it's not exactly a factor of 1/2 off, so even if I'm getting a convention wrong, there's still something else awry.
ETA: fixed the conv_tab printout
~Wait, no, when did the LiH (4e,3o) stop converging??????? argggghhh!~
ETA: fixed
I think it could be the fact that energy_core
is added each time we accumulate.
Take a look at these line
self.vhf_c += mo_coeff.conjugate().T @ ot.get_eff_1body(ao,
weight, vrho_c, non0tab=non0tab, shls_slice=shls_slice,
ao_loc=ao_loc,
hermi=1) @ mo_coeff
self.energy_core = np.trace(self.vhf_c[:ncore, :ncore]) / 2
Each time we call the _accumulate
we append to self.vhf_c
which is fine. But then self.energy_core
gets updated again. I think this is fine when self.paaa_only=False
, but if self.paaa_only=True
then the self.vhf_c
also adds in vhf_a
.
if self.paaa_only:
# 1/2 v_aiuv D_ii D_uv = v^ai_uv D_uv -> F_ai, F_ia
# needs to be in here since it would otherwise be calculated using
# ppaa and papa. This is harmless to the CI problem because the
# elements in the active space and core-core sector are ignored
# below.
eff_rho_a = _contract_eff_rho(eff_Pi, rho_a)
vhf_a = get_eff_1body(ot, ao, weight, eff_rho_a, non0tab=non0tab,
shls_slice=shls_slice, ao_loc=ao_loc, hermi=1)
vhf_a = mo_coeff.conjugate().T @ vhf_a @ mo_coeff
vhf_a[ncore:nocc, :] = vhf_a[:, ncore:nocc] = 0.0
self.vhf_c += vhf_a
Then the next time we accumulate, we are not just accumulating over vhf_c
, but also vhf_c + vhf_a
? My logic here could be a bit off, but it might be something in the details of that. Perhaps it is better to just calculate energy_core
differently?
I Got it!! Edit: And i lost it trying to merge a git. fuck.
Edit 2: Got it again. I think. Going to commit this and link for you to take a look.
Okay, here is the necessary commit info and lines. I fixed the self.energy_core
with the factor of 1/2 as you did.
Additionally, I think I might have been missing the mo_coeff
kwarg in the _dms.casdm1s_to_dm1s()
function which needs to have the new MO coeffs for the core orbitals.
LGTM!
See how this works to use github to ask questions. So in general, I have written out the kernel functions for generating $\Delta\cdot \mathbf{F}$ which have elements of $$\left[\Delta \cdot \mathbf{F}\right]_{pq} = \int \vec{\rho}^t\Delta\mathbf{f}^\mathrm{ot}\frac{\partial \vec{\rho}}{\partial\gamma{pq}}$$ and for the 4-body term as well here. It uses some generalization of the
veff
code which I extracted/reused. I was able to test that thelazy_kernel
andkernel
functions agree using the same method as theveff
code (I used the ao->mo for the 2-electron terms). What I want to do though is test that the $\Delta\cdot \mathbf{F}$ elements agree when you numerically differentiate the $V{pq}$ and $v{pqrs}$ elements and then contract with a density.For testing
pdft_veff
, I generally understand what is happening here. Thegen_g_hop
gets the gradient, gradient update, hessian_update, and hessian_diag elements, right? Does g_all, in the orbital sector (ieg_all[:ngorb]
) contain the $$F{xy} - F{yx}$$ where $F_{xy}$ is the generalized Fock matrix elements given theeris
andget_hcore()
in the current environment?Regardless, you get the
x0
term which is the step in some direction. You then scale that term progressively smaller and checking to see if you get numerical convergence of the difference between the analytical and numerical derivative (summary of these lines). I am a bit confused on whatnp.dot(g_all, x1)
represents here mathematically (which is conflated with what exactlyg_all
is).With that being said, how should I go about testing the
pdft_feff
terms numerically? I understand that you wrote theEotOrbitalHessianOperator
and test this, but is there a simpler way of testing without having to copy a bunch of code from the file? You also discuss needing to use a "dressedgen_g_hop
" object (here).Lastly, I am a bit confused on what exactly
paaa_only
does for the_ERIS
(see these lines). If I understand correctly, you are adding the active space contribution of the HF potential to the HF potential core term? I know this is important since similar terms appear in thegen_g_hop
code and theEotOrbitalHessianOperator
Any help would be appreciated, and let me know if you would rather me ask these questions via a different method of communication.