Skip to content

Commit 1700e24

Browse files
authored
[PBC Resources Estimates 2/4] Add Integral Factorization Helpers (#822)
* Add k-point THC code. * Add k-thc notebook. * Add utilities. * Add reference data. * Add missing __init__ * Add missing init / skipifs. * Fix formatting. * Resolve review comments. * Address comments. * Remove utils. * No more utils. * More review comments. * Fix formatting. * Formatting + add map to gvec_logic. * Fix import issues. * Add utility classes to handler different PBC Hamiltonian factorizations. Add inits. * No more utils. * Tidyup. * Fix formatting. * Add include guards. * Fix inits. * Fix import. * Remove test systems. * Fix format. * Address review comments. * Fixes for checks.
1 parent 04e23a7 commit 1700e24

16 files changed

Lines changed: 1959 additions & 98 deletions
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
# coverage: ignore
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
import itertools
14+
from typing import Tuple
15+
import numpy as np
16+
import numpy.typing as npt
17+
18+
from pyscf.pbc import scf
19+
20+
from openfermion.resource_estimates.pbc.hamiltonian import (
21+
build_momentum_transfer_mapping,)
22+
23+
24+
def get_df_factor(mat: npt.NDArray, thresh: float, verify_adjoint: bool = False
25+
) -> Tuple[npt.NDArray, npt.NDArray]:
26+
"""Represent a matrix via non-zero eigenvalue vector pairs.
27+
28+
Anything above thresh is considered non-zero
29+
30+
Args:
31+
mat: Matrix to double factorize.
32+
thresh: Double factorization eigenvalue threshold
33+
verify_adjoint: Verify input matrix is Hermitian (Default value = False)
34+
35+
Returns:
36+
Tuple eigen values and eigen vectors (lambda, V)
37+
38+
"""
39+
if verify_adjoint:
40+
assert np.allclose(mat, mat.conj().T)
41+
eigs, eigv = np.linalg.eigh(mat)
42+
normSC = np.sum(np.abs(eigs))
43+
ix = np.argsort(np.abs(eigs))[::-1]
44+
eigs = eigs[ix]
45+
eigv = eigv[:, ix]
46+
truncation = normSC * np.abs(eigs)
47+
to_zero = truncation < thresh
48+
eigs[to_zero] = 0.0
49+
eigv[:, to_zero] = 0.0
50+
idx_not_zero = np.where(~to_zero == True)[0]
51+
eigs = eigs[idx_not_zero]
52+
eigv = eigv[:, idx_not_zero]
53+
return eigs, eigv
54+
55+
56+
class DFABKpointIntegrals:
57+
58+
def __init__(self, cholesky_factor: npt.NDArray, kmf: scf.HF):
59+
"""Class defining double factorized ERIs.
60+
61+
We need to form the A and B objects which are indexed by Cholesky index
62+
n and momentum mode Q. This is accomplished by constructing rho[Q, n,
63+
kpt, nao, nao] by reshaping the cholesky object. We don't form the
64+
matrix
65+
66+
Args:
67+
cholesky_factor: Cholesky factor tensor that is
68+
[nkpts, nkpts, naux, nao, nao]
69+
kmf: pyscf k-object. Currently only used to obtain the number of
70+
k-points. must have an attribute kpts which len(self.kmf.kpts)
71+
returns number of kpts.
72+
"""
73+
self.chol = cholesky_factor
74+
self.kmf = kmf
75+
self.nk = len(self.kmf.kpts)
76+
naux = 0
77+
for i, j in itertools.product(range(self.nk), repeat=2):
78+
naux = max(self.chol[i, j].shape[0], naux)
79+
self.naux = naux
80+
self.nao = cholesky_factor[0, 0].shape[-1]
81+
k_transfer_map = build_momentum_transfer_mapping(
82+
self.kmf.cell, self.kmf.kpts)
83+
self.k_transfer_map = k_transfer_map
84+
self.reverse_k_transfer_map = np.zeros_like(
85+
self.k_transfer_map) # [kidx, kmq_idx] = qidx
86+
for kidx in range(self.nk):
87+
for qidx in range(self.nk):
88+
kmq_idx = self.k_transfer_map[qidx, kidx]
89+
self.reverse_k_transfer_map[kidx, kmq_idx] = qidx
90+
91+
# set up for later when we construct DF
92+
self.df_factors = None
93+
self.a_mats = None
94+
self.b_mats = None
95+
96+
def build_A_B_n_q_k_from_chol(self, qidx: int, kidx: int):
97+
"""Builds matrices that are blocks in two momentum indices
98+
99+
k | k-Q |
100+
------------
101+
k | | |
102+
----------------
103+
k-Q | | |
104+
----------------
105+
106+
where the off diagonal blocks are the ones that are populated. All
107+
matrices for every Cholesky vector are constructed.
108+
109+
Args:
110+
qidx: index for momentum mode Q.
111+
kidx: index for momentum mode K.
112+
113+
Returns:
114+
Amat: The `A` matrix in DF (~ L + L^)
115+
Bmat: The `B` matrix in DF (~ i (L - L^)
116+
"""
117+
k_minus_q_idx = self.k_transfer_map[qidx, kidx]
118+
naux = self.chol[kidx, k_minus_q_idx].shape[0]
119+
nmo = self.nao
120+
Amat = np.zeros((naux, 2 * nmo, 2 * nmo), dtype=np.complex128)
121+
Bmat = np.zeros((naux, 2 * nmo, 2 * nmo), dtype=np.complex128)
122+
if k_minus_q_idx == kidx:
123+
Amat[:, :nmo, :nmo] = self.chol[
124+
kidx, k_minus_q_idx] # beacuse L_{pK, qK,n}= L_{qK,pK,n}^{*}
125+
Bmat[:, :nmo, :nmo] = 0.5j * (
126+
self.chol[kidx, k_minus_q_idx] -
127+
self.chol[kidx, k_minus_q_idx].conj().transpose(0, 2, 1))
128+
else:
129+
Amat[:, :nmo, nmo:] = (0.5 * self.chol[kidx, k_minus_q_idx]
130+
) # [naux, nmo, nmo]
131+
Amat[:, nmo:, :nmo] = 0.5 * self.chol[kidx, k_minus_q_idx].conj(
132+
).transpose(0, 2, 1)
133+
134+
Bmat[:, :nmo, nmo:] = (0.5j * self.chol[kidx, k_minus_q_idx]
135+
) # [naux, nmo, nmo]
136+
Bmat[:, nmo:, :nmo] = -0.5j * self.chol[kidx, k_minus_q_idx].conj(
137+
).transpose(0, 2, 1)
138+
139+
return Amat, Bmat
140+
141+
def build_chol_part_from_A_B(
142+
self,
143+
kidx: int,
144+
qidx: int,
145+
Amats: npt.NDArray,
146+
Bmats: npt.NDArray,
147+
) -> npt.NDArray:
148+
r"""Construct rho_{n, k, Q}.
149+
150+
Args:
151+
kidx: k-momentum index
152+
qidx: Q-momentum index
153+
Amats: naux, 2 * nmo, 2 * nmo]
154+
Bmats: naux, 2 * nmo, 2 * nmo]
155+
156+
Returns:
157+
cholesky factors: 3-tensors (k, k-Q)[naux, nao, nao],
158+
(kp, kp-Q)[naux, nao, nao]
159+
160+
"""
161+
k_minus_q_idx = self.k_transfer_map[qidx, kidx]
162+
nmo = self.nao
163+
if k_minus_q_idx == kidx:
164+
return Amats[:, :nmo, :nmo]
165+
else:
166+
return Amats[:, :nmo, nmo:] + -1j * Bmats[:, :nmo, nmo:]
167+
168+
def double_factorize(self, thresh=None) -> None:
169+
"""Construct a double factorization of the Hamiltonian.
170+
171+
Iterate through qidx, kidx and get factorized Amat and Bmat for each
172+
Cholesky rank
173+
174+
Args:
175+
thresh: Double factorization eigenvalue threshold
176+
(Default value = None)
177+
"""
178+
if thresh is None:
179+
thresh = 1.0e-13
180+
if self.df_factors is not None:
181+
return self.df_factors
182+
183+
nkpts = self.nk
184+
nmo = self.nao
185+
naux = self.naux
186+
self.amat_n_mats = np.zeros((nkpts, nkpts, naux, 2 * nmo, 2 * nmo),
187+
dtype=np.complex128)
188+
self.bmat_n_mats = np.zeros((nkpts, nkpts, naux, 2 * nmo, 2 * nmo),
189+
dtype=np.complex128)
190+
self.amat_lambda_vecs = np.empty((nkpts, nkpts, naux), dtype=object)
191+
self.bmat_lambda_vecs = np.empty((nkpts, nkpts, naux), dtype=object)
192+
for qidx, kidx in itertools.product(range(nkpts), repeat=2):
193+
Amats, Bmats = self.build_A_B_n_q_k_from_chol(qidx, kidx)
194+
naux_qk = Amats.shape[0]
195+
assert naux_qk <= naux
196+
for nc in range(naux_qk):
197+
amat_n_eigs, amat_n_eigv = get_df_factor(Amats[nc], thresh)
198+
self.amat_n_mats[kidx, qidx][nc, :, :] = (
199+
amat_n_eigv @ np.diag(amat_n_eigs) @ amat_n_eigv.conj().T)
200+
self.amat_lambda_vecs[kidx, qidx, nc] = amat_n_eigs
201+
202+
bmat_n_eigs, bmat_n_eigv = get_df_factor(Bmats[nc], thresh)
203+
self.bmat_n_mats[kidx, qidx][nc, :, :] = (
204+
bmat_n_eigv @ np.diag(bmat_n_eigs) @ bmat_n_eigv.conj().T)
205+
self.bmat_lambda_vecs[kidx, qidx, nc] = bmat_n_eigs
206+
207+
def get_eri(self, ikpts: list) -> npt.NDArray:
208+
"""Construct (pkp qkq| rkr sks) via A and B tensors.
209+
210+
Args:
211+
ikpts: list of four integers representing the index of the kpoint in
212+
self.kmf.kpts
213+
214+
Returns:
215+
eris: ([pkp][qkq]|[rkr][sks])
216+
"""
217+
ikp, ikq, ikr, iks = ikpts # (k, k-q, k'-q, k')
218+
qidx = self.reverse_k_transfer_map[ikp, ikq]
219+
test_qidx = self.reverse_k_transfer_map[iks, ikr]
220+
assert test_qidx == qidx
221+
222+
# build Cholesky vector from truncated A and B
223+
chol_val_k_kmq = self.build_chol_part_from_A_B(
224+
ikp, qidx, self.amat_n_mats[ikp, qidx], self.bmat_n_mats[ikp, qidx])
225+
chol_val_kp_kpmq = self.build_chol_part_from_A_B(
226+
iks, qidx, self.amat_n_mats[iks, qidx], self.bmat_n_mats[iks, qidx])
227+
228+
return np.einsum(
229+
"npq,nsr->pqrs",
230+
chol_val_k_kmq,
231+
chol_val_kp_kpmq.conj(),
232+
optimize=True,
233+
)
234+
235+
def get_eri_exact(self, ikpts: list) -> npt.NDArray:
236+
"""Construct (pkp qkq| rkr sks) exactly from Cholesky vector.
237+
238+
This is for constructing the J and K like terms needed for the one-body
239+
component lambda
240+
241+
Args:
242+
ikpts: list of four integers representing the index of the kpoint in
243+
self.kmf.kpts
244+
245+
Returns:
246+
eris: ([pkp][qkq]|[rkr][sks])
247+
"""
248+
ikp, ikq, ikr, iks = ikpts
249+
return np.einsum(
250+
"npq,nsr->pqrs",
251+
self.chol[ikp, ikq],
252+
self.chol[iks, ikr].conj(),
253+
optimize=True,
254+
)
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# coverage: ignore
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
import itertools
14+
15+
import numpy as np
16+
import pytest
17+
18+
from openfermion.resource_estimates import HAVE_DEPS_FOR_RESOURCE_ESTIMATES
19+
20+
if HAVE_DEPS_FOR_RESOURCE_ESTIMATES:
21+
from pyscf.pbc import mp
22+
23+
from openfermion.resource_estimates.pbc.testing import (
24+
make_diamond_113_szv,)
25+
from openfermion.resource_estimates.pbc.df.df_integrals import (
26+
DFABKpointIntegrals,)
27+
from openfermion.resource_estimates.pbc.hamiltonian import (
28+
cholesky_from_df_ints,)
29+
30+
31+
@pytest.mark.skipif(not HAVE_DEPS_FOR_RESOURCE_ESTIMATES,
32+
reason='pyscf and/or jax not installed.')
33+
def test_df_amat_bmat():
34+
mf = make_diamond_113_szv()
35+
mymp = mp.KMP2(mf)
36+
nmo = mymp.nmo
37+
38+
Luv = cholesky_from_df_ints(mymp) # [kpt, kpt, naux, nao, nao]
39+
dfk_inst = DFABKpointIntegrals(Luv.copy(), mf)
40+
naux = dfk_inst.naux
41+
42+
dfk_inst.double_factorize()
43+
44+
nkpts = len(mf.kpts)
45+
for qidx, kidx in itertools.product(range(nkpts), repeat=2):
46+
Amats, Bmats = dfk_inst.build_A_B_n_q_k_from_chol(qidx, kidx)
47+
# check if Amats and Bmats have the correct size
48+
assert Amats.shape == (naux, 2 * nmo, 2 * nmo)
49+
assert Bmats.shape == (naux, 2 * nmo, 2 * nmo)
50+
51+
# check if Amats and Bmats have the correct symmetry--Hermitian
52+
assert np.allclose(Amats, Amats.conj().transpose(0, 2, 1))
53+
assert np.allclose(Bmats, Bmats.conj().transpose(0, 2, 1))
54+
55+
# check if we can recover the Cholesky vector from Amat
56+
k_minus_q_idx = dfk_inst.k_transfer_map[qidx, kidx]
57+
test_chol = dfk_inst.build_chol_part_from_A_B(kidx, qidx, Amats, Bmats)
58+
assert np.allclose(test_chol, dfk_inst.chol[kidx, k_minus_q_idx])
59+
60+
# check if factorized is working numerically exact case
61+
assert np.allclose(dfk_inst.amat_n_mats[kidx, qidx], Amats)
62+
assert np.allclose(dfk_inst.bmat_n_mats[kidx, qidx], Bmats)
63+
64+
for nn in range(Amats.shape[0]):
65+
w, v = np.linalg.eigh(Amats[nn, :, :])
66+
non_zero_idx = np.where(w > 1.0e-4)[0]
67+
w = w[non_zero_idx]
68+
v = v[:, non_zero_idx]
69+
assert len(w) <= 2 * nmo
70+
71+
for qidx in range(nkpts):
72+
for nn in range(naux):
73+
for kidx in range(nkpts):
74+
eigs_a_fixed_n_q = dfk_inst.amat_lambda_vecs[kidx, qidx, nn]
75+
eigs_b_fixed_n_q = dfk_inst.bmat_lambda_vecs[kidx, qidx, nn]
76+
assert len(eigs_a_fixed_n_q) <= 2 * nmo
77+
assert len(eigs_b_fixed_n_q) <= 2 * nmo
78+
79+
for kidx in range(nkpts):
80+
for kpidx in range(nkpts):
81+
for qidx in range(nkpts):
82+
kmq_idx = dfk_inst.k_transfer_map[qidx, kidx]
83+
kpmq_idx = dfk_inst.k_transfer_map[qidx, kpidx]
84+
exact_eri_block = dfk_inst.get_eri_exact(
85+
[kidx, kmq_idx, kpmq_idx, kpidx])
86+
test_eri_block = dfk_inst.get_eri(
87+
[kidx, kmq_idx, kpmq_idx, kpidx])
88+
assert np.allclose(exact_eri_block, test_eri_block)

0 commit comments

Comments
 (0)