Skip to content

Commit ea144a6

Browse files
authored
Support complex two-body tensor linear operator (#279)
* support linop for complex two-body tensor * require pyscf >= 2.7 * restore single-factorized hamiltonian tests single- or double- factorization does not support complex two-body operators
1 parent 7a5e6a5 commit ea144a6

File tree

4 files changed

+15
-87
lines changed

4 files changed

+15
-87
lines changed

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ dependencies = [
2626
"numpy",
2727
"opt_einsum",
2828
"orjson",
29-
"pyscf >= 2.4",
29+
"pyscf >= 2.7",
3030
"qiskit >= 1.1",
3131
"scipy",
3232
"typing-extensions",

python/ffsim/hamiltonians/molecular_hamiltonian.py

+12-78
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import pyscf.ao2mo
1919
import pyscf.tools
2020
from opt_einsum import contract
21-
from pyscf.fci.direct_nosym import absorb_h1e, contract_1e, contract_2e, make_hdiag
21+
from pyscf.fci.direct_nosym import absorb_h1e, contract_2e, make_hdiag
2222
from scipy.sparse.linalg import LinearOperator
2323
from typing_extensions import deprecated
2424

@@ -122,34 +122,22 @@ def rotated(self, orbital_rotation: np.ndarray) -> MolecularHamiltonian:
122122

123123
def _linear_operator_(self, norb: int, nelec: tuple[int, int]) -> LinearOperator:
124124
"""Return a SciPy LinearOperator representing the object."""
125-
if np.iscomplexobj(self.two_body_tensor):
126-
raise NotImplementedError(
127-
"This Hamiltonian has a complex-valued two-body tensor. "
128-
"LinearOperator support for complex two-body tensors is not yet "
129-
"implemented. See https://github.com/qiskit-community/ffsim/issues/81."
130-
)
131-
132125
n_alpha, n_beta = nelec
133126
linkstr_index_a = gen_linkstr_index(range(norb), n_alpha)
134127
linkstr_index_b = gen_linkstr_index(range(norb), n_beta)
135128
link_index = (linkstr_index_a, linkstr_index_b)
129+
two_body = absorb_h1e(
130+
self.one_body_tensor, self.two_body_tensor, norb, nelec, 0.5
131+
)
136132

137-
if np.iscomplexobj(self.one_body_tensor):
138-
return _linear_operator_complex(
139-
one_body_tensor=self.one_body_tensor,
140-
two_body_tensor=self.two_body_tensor,
141-
constant=self.constant,
142-
norb=norb,
143-
nelec=nelec,
144-
link_index=link_index,
145-
)
146-
return _linear_operator_real(
147-
one_body_tensor=self.one_body_tensor,
148-
two_body_tensor=self.two_body_tensor,
149-
constant=self.constant,
150-
norb=norb,
151-
nelec=nelec,
152-
link_index=link_index,
133+
def matvec(vec: np.ndarray):
134+
result = self.constant * vec.astype(complex, copy=False)
135+
result += contract_2e(two_body, vec, norb, nelec, link_index=link_index)
136+
return result
137+
138+
dim_ = dim(norb, nelec)
139+
return LinearOperator(
140+
shape=(dim_, dim_), matvec=matvec, rmatvec=matvec, dtype=complex
153141
)
154142

155143
def _diag_(self, norb: int, nelec: tuple[int, int]) -> np.ndarray:
@@ -203,57 +191,3 @@ def _approx_eq_(self, other, rtol: float, atol: float) -> bool:
203191
return False
204192
return True
205193
return NotImplemented
206-
207-
208-
def _linear_operator_complex(
209-
one_body_tensor: np.ndarray,
210-
two_body_tensor: np.ndarray,
211-
constant: float,
212-
norb: int,
213-
nelec: tuple[int, int],
214-
link_index: tuple[np.ndarray, np.ndarray],
215-
):
216-
two_body = absorb_h1e(one_body_tensor.real, two_body_tensor, norb, nelec, 0.5)
217-
dim_ = dim(norb, nelec)
218-
219-
def matvec(vec: np.ndarray):
220-
result = constant * vec.astype(complex, copy=False)
221-
result += 1j * contract_1e(
222-
one_body_tensor.imag, vec.real, norb, nelec, link_index=link_index
223-
)
224-
result -= contract_1e(
225-
one_body_tensor.imag, vec.imag, norb, nelec, link_index=link_index
226-
)
227-
result += contract_2e(two_body, vec.real, norb, nelec, link_index=link_index)
228-
result += 1j * contract_2e(
229-
two_body, vec.imag, norb, nelec, link_index=link_index
230-
)
231-
return result
232-
233-
return LinearOperator(
234-
shape=(dim_, dim_), matvec=matvec, rmatvec=matvec, dtype=complex
235-
)
236-
237-
238-
def _linear_operator_real(
239-
one_body_tensor: np.ndarray,
240-
two_body_tensor: np.ndarray,
241-
constant: float,
242-
norb: int,
243-
nelec: tuple[int, int],
244-
link_index: tuple[np.ndarray, np.ndarray],
245-
):
246-
two_body = absorb_h1e(one_body_tensor, two_body_tensor, norb, nelec, 0.5)
247-
dim_ = dim(norb, nelec)
248-
249-
def matvec(vec: np.ndarray):
250-
result = constant * vec.astype(complex, copy=False)
251-
result += contract_2e(two_body, vec.real, norb, nelec, link_index=link_index)
252-
result += 1j * contract_2e(
253-
two_body, vec.imag, norb, nelec, link_index=link_index
254-
)
255-
return result
256-
257-
return LinearOperator(
258-
shape=(dim_, dim_), matvec=matvec, rmatvec=matvec, dtype=complex
259-
)

tests/python/hamiltonians/molecular_hamiltonian_test.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,7 @@ def test_fermion_operator(norb: int, nelec: tuple[int, int]):
106106
rng = np.random.default_rng()
107107

108108
one_body_tensor = ffsim.random.random_hermitian(norb, seed=rng)
109-
# TODO remove dtype=float after adding support for complex
110-
two_body_tensor = ffsim.random.random_two_body_tensor(norb, seed=rng, dtype=float)
109+
two_body_tensor = ffsim.random.random_two_body_tensor(norb, seed=rng)
111110
constant = rng.standard_normal()
112111
mol_hamiltonian = ffsim.MolecularHamiltonian(
113112
one_body_tensor, two_body_tensor, constant=constant
@@ -132,8 +131,7 @@ def test_rotated():
132131

133132
# generate a random molecular Hamiltonian
134133
one_body_tensor = ffsim.random.random_hermitian(norb, seed=rng)
135-
# TODO remove dtype=float after adding support for complex
136-
two_body_tensor = ffsim.random.random_two_body_tensor(norb, seed=rng, dtype=float)
134+
two_body_tensor = ffsim.random.random_two_body_tensor(norb, seed=rng)
137135
constant = rng.standard_normal()
138136
mol_hamiltonian = ffsim.MolecularHamiltonian(
139137
one_body_tensor, two_body_tensor, constant=constant

tests/python/hamiltonians/single_factorized_hamiltonian_test.py

-4
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ def test_linear_operator(norb: int, nelec: tuple[int, int], cholesky: bool):
3333
"""Test linear operator."""
3434
rng = np.random.default_rng(2474)
3535

36-
# TODO remove dtype=float once complex two-body is supported
3736
mol_hamiltonian = ffsim.random.random_molecular_hamiltonian(
3837
norb, seed=rng, dtype=float
3938
)
@@ -58,7 +57,6 @@ def test_reduced_matrix_product_states(norb: int, nelec: tuple[int, int]):
5857
"""Test computing reduced matrix on product states."""
5958
rng = np.random.default_rng(7869)
6059

61-
# TODO remove dtype=float once complex two-body is supported
6260
mol_hamiltonian = ffsim.random.random_molecular_hamiltonian(
6361
norb, seed=rng, dtype=float
6462
)
@@ -104,7 +102,6 @@ def test_expectation_product_state_slater_determinant(
104102
"""Test computing expectation value on Slater determinant product state."""
105103
rng = np.random.default_rng(3400)
106104

107-
# TODO remove dtype=float once complex two-body is supported
108105
mol_hamiltonian = ffsim.random.random_molecular_hamiltonian(
109106
norb, seed=rng, dtype=float
110107
)
@@ -137,7 +134,6 @@ def test_expectation_product_state(norb: int, nelec: tuple[int, int]):
137134
"""Test computing expectation value on product state."""
138135
rng = np.random.default_rng(6775)
139136

140-
# TODO remove dtype=float once complex two-body is supported
141137
mol_hamiltonian = ffsim.random.random_molecular_hamiltonian(
142138
norb, seed=rng, dtype=float
143139
)

0 commit comments

Comments
 (0)