Skip to content

Commit 37c468e

Browse files
committed
Add utility classes to handler different PBC Hamiltonian factorizations.
Add inits.
1 parent 22c833d commit 37c468e

15 files changed

+1885
-2
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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 pytest
14+
15+
try:
16+
import pyscf
17+
except (ImportError, ModuleNotFoundError) as err:
18+
pytest.skip(f"Need pyscf for PBC resource estimates {err}",
19+
allow_module_level=True)
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
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.utils.hamiltonian_utils 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+
anything above thresh is considered non-zero
28+
29+
Args:
30+
mat: Matrix to double factorize.
31+
thresh: Double factorization eigenvalue threshold
32+
verify_adjoint: Verify input matrix is Hermitian (Default value = False)
33+
34+
Returns:
35+
Tuple eigen values and eigen vectors (lambda, V)
36+
37+
"""
38+
if verify_adjoint:
39+
assert np.allclose(mat, mat.conj().T)
40+
eigs, eigv = np.linalg.eigh(mat)
41+
normSC = np.sum(np.abs(eigs))
42+
ix = np.argsort(np.abs(eigs))[::-1]
43+
eigs = eigs[ix]
44+
eigv = eigv[:, ix]
45+
truncation = normSC * np.abs(eigs)
46+
to_zero = truncation < thresh
47+
eigs[to_zero] = 0.0
48+
eigv[:, to_zero] = 0.0
49+
idx_not_zero = np.where(~to_zero == True)[0]
50+
eigs = eigs[idx_not_zero]
51+
eigv = eigv[:, idx_not_zero]
52+
return eigs, eigv
53+
54+
55+
class DFABKpointIntegrals:
56+
57+
def __init__(self, cholesky_factor: npt.NDArray, kmf: scf.HF):
58+
"""Initialize a ERI object for CCSD from Cholesky factors and a
59+
pyscf mean-field object
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, kidx):
97+
"""Builds matrices that are block 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 is constructed.
108+
109+
Args:
110+
qidx: index for momentum mode Q.
111+
kidx: index for momentum mode K.
112+
113+
Returns:
114+
Amat: A matrix
115+
Bmat: A matrix
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+
"""Construct rho_{n, k, Q} which is equal to the cholesky factor by
149+
summing together via the following relationships
150+
151+
Args:
152+
kidx: k-momentum index
153+
qidx: Q-momentum index
154+
Amats: naux, 2 * nmo, 2 * nmo]
155+
Bmats: naux, 2 * nmo, 2 * nmo]
156+
157+
Returns:
158+
cholesky factors 3-tensors (k, k-Q)[naux, nao, nao],
159+
(kp, kp-Q)[naux, nao, nao]
160+
161+
"""
162+
k_minus_q_idx = self.k_transfer_map[qidx, kidx]
163+
nmo = self.nao
164+
if k_minus_q_idx == kidx:
165+
return Amats[:, :nmo, :nmo]
166+
else:
167+
return Amats[:, :nmo, nmo:] + -1j * Bmats[:, :nmo, nmo:]
168+
169+
def double_factorize(self, thresh=None) -> None:
170+
"""construct a double factorization of the Hamiltonian.
171+
172+
Iterate through qidx, kidx and get factorized Amat and Bmat for each
173+
Cholesky rank
174+
175+
Args:
176+
thresh: Double factorization eigenvalue threshold
177+
(Default value = None)
178+
"""
179+
if thresh is None:
180+
thresh = 1.0e-13
181+
if self.df_factors is not None:
182+
return self.df_factors
183+
184+
nkpts = self.nk
185+
nmo = self.nao
186+
naux = self.naux
187+
self.amat_n_mats = np.zeros((nkpts, nkpts, naux, 2 * nmo, 2 * nmo),
188+
dtype=np.complex128)
189+
self.bmat_n_mats = np.zeros((nkpts, nkpts, naux, 2 * nmo, 2 * nmo),
190+
dtype=np.complex128)
191+
self.amat_lambda_vecs = np.empty((nkpts, nkpts, naux), dtype=object)
192+
self.bmat_lambda_vecs = np.empty((nkpts, nkpts, naux), dtype=object)
193+
for qidx, kidx in itertools.product(range(nkpts), repeat=2):
194+
Amats, Bmats = self.build_A_B_n_q_k_from_chol(qidx, kidx)
195+
naux_qk = Amats.shape[0]
196+
assert naux_qk <= naux
197+
for nc in range(naux_qk):
198+
amat_n_eigs, amat_n_eigv = get_df_factor(Amats[nc], thresh)
199+
self.amat_n_mats[kidx, qidx][nc, :, :] = (
200+
amat_n_eigv @ np.diag(amat_n_eigs) @ amat_n_eigv.conj().T)
201+
self.amat_lambda_vecs[kidx, qidx, nc] = amat_n_eigs
202+
203+
bmat_n_eigs, bmat_n_eigv = get_df_factor(Bmats[nc], thresh)
204+
self.bmat_n_mats[kidx, qidx][nc, :, :] = (
205+
bmat_n_eigv @ np.diag(bmat_n_eigs) @ bmat_n_eigv.conj().T)
206+
self.bmat_lambda_vecs[kidx, qidx, nc] = bmat_n_eigs
207+
208+
return
209+
210+
def get_eri(self, ikpts: list) -> npt.NDArray:
211+
"""Construct (pkp qkq| rkr sks) via A and B tensors that have already
212+
been constructed
213+
214+
Args:
215+
ikpts: list of four integers representing the index of the kpoint in
216+
self.kmf.kpts
217+
218+
Returns:
219+
eris: ([pkp][qkq]|[rkr][sks])
220+
"""
221+
ikp, ikq, ikr, iks = ikpts # (k, k-q, k'-q, k')
222+
qidx = self.reverse_k_transfer_map[ikp, ikq]
223+
test_qidx = self.reverse_k_transfer_map[iks, ikr]
224+
assert test_qidx == qidx
225+
226+
# build Cholesky vector from truncated A and B
227+
chol_val_k_kmq = self.build_chol_part_from_A_B(
228+
ikp, qidx, self.amat_n_mats[ikp, qidx], self.bmat_n_mats[ikp, qidx])
229+
chol_val_kp_kpmq = self.build_chol_part_from_A_B(
230+
iks, qidx, self.amat_n_mats[iks, qidx], self.bmat_n_mats[iks, qidx])
231+
232+
return np.einsum(
233+
"npq,nsr->pqrs",
234+
chol_val_k_kmq,
235+
chol_val_kp_kpmq.conj(),
236+
optimize=True,
237+
)
238+
239+
def get_eri_exact(self, ikpts: list) -> npt.NDArray:
240+
"""Construct (pkp qkq| rkr sks) exactly from Cholesky vector. This is
241+
for constructing the J and K like terms needed for the one-body
242+
component lambda
243+
244+
Args:
245+
ikpts: list of four integers representing the index of the kpoint in
246+
self.kmf.kpts
247+
248+
Returns:
249+
eris: ([pkp][qkq]|[rkr][sks])
250+
"""
251+
ikp, ikq, ikr, iks = ikpts
252+
return np.einsum(
253+
"npq,nsr->pqrs",
254+
self.chol[ikp, ikq],
255+
self.chol[iks, ikr].conj(),
256+
optimize=True,
257+
)
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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+
17+
from pyscf.pbc import mp
18+
19+
from openfermion.resource_estimates.pbc.utils.test_utils import (
20+
make_diamond_113_szv,)
21+
from openfermion.resource_estimates.pbc.df.integral_helper_df import (
22+
DFABKpointIntegrals,)
23+
from openfermion.resource_estimates.pbc.utils.hamiltonian_utils import (
24+
cholesky_from_df_ints,)
25+
26+
27+
def test_df_amat_bmat():
28+
mf = make_diamond_113_szv()
29+
mymp = mp.KMP2(mf)
30+
nmo = mymp.nmo
31+
32+
Luv = cholesky_from_df_ints(mymp) # [kpt, kpt, naux, nao, nao]
33+
dfk_inst = DFABKpointIntegrals(Luv.copy(), mf)
34+
naux = dfk_inst.naux
35+
36+
dfk_inst.double_factorize()
37+
38+
import openfermion as of
39+
40+
nkpts = len(mf.kpts)
41+
for qidx, kidx in itertools.product(range(nkpts), repeat=2):
42+
Amats, Bmats = dfk_inst.build_A_B_n_q_k_from_chol(qidx, kidx)
43+
# check if Amats and Bmats have the correct size
44+
assert Amats.shape == (naux, 2 * nmo, 2 * nmo)
45+
assert Bmats.shape == (naux, 2 * nmo, 2 * nmo)
46+
47+
# check if Amats and Bmats have the correct symmetry--Hermitian
48+
assert np.allclose(Amats, Amats.conj().transpose(0, 2, 1))
49+
assert np.allclose(Bmats, Bmats.conj().transpose(0, 2, 1))
50+
51+
# check if we can recover the Cholesky vector from Amat
52+
k_minus_q_idx = dfk_inst.k_transfer_map[qidx, kidx]
53+
test_chol = dfk_inst.build_chol_part_from_A_B(kidx, qidx, Amats, Bmats)
54+
assert np.allclose(test_chol, dfk_inst.chol[kidx, k_minus_q_idx])
55+
56+
# check if factorized is working numerically exact case
57+
assert np.allclose(dfk_inst.amat_n_mats[kidx, qidx], Amats)
58+
assert np.allclose(dfk_inst.bmat_n_mats[kidx, qidx], Bmats)
59+
60+
for nn in range(Amats.shape[0]):
61+
w, v = np.linalg.eigh(Amats[nn, :, :])
62+
non_zero_idx = np.where(w > 1.0e-4)[0]
63+
w = w[non_zero_idx]
64+
v = v[:, non_zero_idx]
65+
assert len(w) <= 2 * nmo
66+
67+
for qidx in range(nkpts):
68+
for nn in range(naux):
69+
for kidx in range(nkpts):
70+
eigs_a_fixed_n_q = dfk_inst.amat_lambda_vecs[kidx, qidx, nn]
71+
eigs_b_fixed_n_q = dfk_inst.bmat_lambda_vecs[kidx, qidx, nn]
72+
assert len(eigs_a_fixed_n_q) <= 2 * nmo
73+
assert len(eigs_b_fixed_n_q) <= 2 * nmo
74+
75+
for kidx in range(nkpts):
76+
for kpidx in range(nkpts):
77+
for qidx in range(nkpts):
78+
kmq_idx = dfk_inst.k_transfer_map[qidx, kidx]
79+
kpmq_idx = dfk_inst.k_transfer_map[qidx, kpidx]
80+
exact_eri_block = dfk_inst.get_eri_exact(
81+
[kidx, kmq_idx, kpmq_idx, kpidx])
82+
test_eri_block = dfk_inst.get_eri(
83+
[kidx, kmq_idx, kpmq_idx, kpidx])
84+
assert np.allclose(exact_eri_block, test_eri_block)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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 pytest
14+
15+
try:
16+
import pyscf
17+
except (ImportError, ModuleNotFoundError) as err:
18+
pytest.skip(f"Need pyscf for PBC resource estimates {err}",
19+
allow_module_level=True)

0 commit comments

Comments
 (0)