Skip to content

Commit 26a4332

Browse files
committed
Fix TypeError on spin-exchange Hamiltonians in low-rank Trotter decomposition
When spin_basis=True, get_chemist_two_body_coefficients silently assumed a spin-symmetric two-body tensor, then raised a cryptic TypeError if the assumption failed. This change: - Adds an explicit spin-symmetry validation in get_chemist_two_body_coefficients: the extracted alpha-alpha-beta-beta block is checked for matrix symmetry before the spin downfolding proceeds. Asymmetric tensors (e.g. spin-exchange Hamiltonians) now raise a ValueError with an informative message. - Changes the downstream check in low_rank_two_body_decomposition from TypeError to ValueError, which is the correct exception type for a data-value violation. - Documents the spin-symmetry requirement in LowRankTrotterAlgorithm. - Adds tests covering spin-exchange input (must raise ValueError), spin-symmetric input (must succeed), and complex tensor input.
1 parent 12d253a commit 26a4332

4 files changed

Lines changed: 104 additions & 7 deletions

File tree

src/openfermion/circuits/low_rank.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,13 @@ def get_chemist_two_body_coefficients(two_body_coefficients, spin_basis=True):
4444
giving the $g_{pqrs}$ tensor in chemist notation.
4545
4646
Raises:
47-
TypeError: Input must be two-body number conserving
48-
FermionOperator or InteractionOperator.
47+
ValueError: Input two-body tensor is not spin-symmetric. The LOW_RANK
48+
decomposition requires a spin-symmetric interaction when
49+
``spin_basis=True``. A spin-symmetric interaction satisfies
50+
``h[p,q,r,s] == h[p+1,q+1,r+1,s+1]`` for all even p, q, r, s
51+
(i.e., the same coefficient for alpha and beta spin channels).
52+
Consider passing ``spin_basis=False`` if your Hamiltonian is not
53+
spin-symmetric.
4954
"""
5055
# Initialize.
5156
n_orbitals = two_body_coefficients.shape[0]
@@ -57,10 +62,34 @@ def get_chemist_two_body_coefficients(two_body_coefficients, spin_basis=True):
5762
n_orbitals = n_orbitals // 2
5863
alpha_indices = list(range(0, n_orbitals * 2, 2))
5964
beta_indices = list(range(1, n_orbitals * 2, 2))
60-
chemist_two_body_coefficients = chemist_two_body_coefficients[
65+
# Extract the αα→ββ block, which is the only block used by the
66+
# spatial-orbital low-rank decomposition.
67+
alpha_alpha_beta_beta = chemist_two_body_coefficients[
6168
numpy.ix_(alpha_indices, alpha_indices, beta_indices, beta_indices)
6269
]
6370

71+
# Validate spin-symmetry by checking that the extracted block is
72+
# symmetric when reshaped to a matrix. For a spin-symmetric interaction
73+
# the chemist tensor satisfies g[p,q,r,s] == g[r,s,p,q] (up to
74+
# permutation symmetry), which makes the reshaped matrix symmetric.
75+
# General spin-dependent interactions such as spin-exchange Hamiltonians
76+
# produce an asymmetric matrix here and cannot be handled by this
77+
# spatial-orbital downfolding approach.
78+
flat = numpy.reshape(alpha_alpha_beta_beta, (n_orbitals**2, n_orbitals**2))
79+
spin_asymmetry = numpy.sum(numpy.absolute(flat - flat.T))
80+
if spin_asymmetry > EQ_TOLERANCE:
81+
raise ValueError(
82+
'The two-body tensor is not spin-symmetric. The LOW_RANK '
83+
'decomposition requires a spin-symmetric interaction when '
84+
'spin_basis=True (i.e., the same coefficients for alpha and '
85+
'beta spin channels). Spin-dependent interactions such as '
86+
'spin-exchange Hamiltonians violate this requirement. '
87+
'Consider passing spin_basis=False if your Hamiltonian is '
88+
'not spin-symmetric.'
89+
)
90+
91+
chemist_two_body_coefficients = alpha_alpha_beta_beta
92+
6493
# Determine a one body correction in the spin basis from spatial basis.
6594
one_body_correction = numpy.zeros((2 * n_orbitals, 2 * n_orbitals), complex)
6695
for p, q, r, s in itertools.product(range(n_orbitals), repeat=4):
@@ -106,7 +135,11 @@ def low_rank_two_body_decomposition(
106135
$\sum_{l=0}^{L-1} (\sum_{pq} |g_{lpq}|)^2 |\lambda_l| < x$
107136
108137
Raises:
109-
TypeError: Invalid two-body coefficient tensor specification.
138+
ValueError: The two-body tensor failed symmetry or reality checks
139+
required for the low-rank decomposition. When ``spin_basis=True``,
140+
the tensor must be spin-symmetric (see
141+
:func:`get_chemist_two_body_coefficients`). When ``spin_basis=False``,
142+
the chemist-ordered tensor must be real and symmetric.
110143
"""
111144
# Initialize N^2 by N^2 interaction array.
112145
one_body_correction, chemist_two_body_coefficients = get_chemist_two_body_coefficients(
@@ -120,7 +153,12 @@ def low_rank_two_body_decomposition(
120153
asymmetry = numpy.sum(numpy.absolute(interaction_array - interaction_array.transpose()))
121154
imaginary_norm = numpy.sum(numpy.absolute(interaction_array.imag))
122155
if asymmetry > EQ_TOLERANCE or imaginary_norm > EQ_TOLERANCE:
123-
raise TypeError('Invalid two-body coefficient tensor specification.')
156+
raise ValueError(
157+
'The two-body coefficient tensor failed the symmetry or reality '
158+
'checks required by the low-rank decomposition. If spin_basis=True, '
159+
'ensure the Hamiltonian is spin-symmetric. If spin_basis=False, '
160+
'ensure the chemist-ordered two-body tensor is real and symmetric.'
161+
)
124162

125163
# Decompose with exact diagonalization.
126164
eigenvalues, eigenvectors = numpy.linalg.eigh(interaction_array)

src/openfermion/circuits/low_rank_test.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,8 @@ def test_molecular_operator_consistency(self):
163163
)
164164
self.assertAlmostEqual(trunc_error, 0.0)
165165

166-
# Check for property errors
167-
with self.assertRaises(TypeError):
166+
# Check for property errors — imaginary tensor should raise ValueError
167+
with self.assertRaises(ValueError):
168168
eigenvalues, one_body_squares, _, trunc_error = low_rank_two_body_decomposition(
169169
two_body_coefficients + 0.01j, truncation_threshold=1.0, final_rank=1
170170
)
@@ -315,3 +315,39 @@ def test_one_body_squared_nonhermitian_raises_error(self):
315315
one_body_matrix = numpy.array([[0, 1], [0, 0]])
316316
with self.assertRaises(ValueError):
317317
prepare_one_body_squared_evolution(one_body_matrix, spin_basis=False)
318+
319+
320+
class SpinExchangeTest(unittest.TestCase):
321+
"""Tests for spin-symmetry validation in the low-rank decomposition."""
322+
323+
def _make_spin_exchange_tensor(self):
324+
"""Return the two_body_tensor for H = a^dag_0 a^dag_3 a_1 a_2 + h.c."""
325+
from openfermion import get_interaction_operator
326+
327+
h_sf = FermionOperator('0^ 3^ 1 2', 1.0) + FermionOperator('2^ 1^ 3 0', 1.0)
328+
return get_interaction_operator(h_sf, n_qubits=4).two_body_tensor
329+
330+
def test_get_chemist_two_body_coefficients_raises_for_spin_exchange(self):
331+
"""Non-spin-symmetric input raises ValueError with informative message."""
332+
two_body_tensor = self._make_spin_exchange_tensor()
333+
with self.assertRaises(ValueError) as ctx:
334+
get_chemist_two_body_coefficients(two_body_tensor, spin_basis=True)
335+
self.assertIn('spin-symmetric', str(ctx.exception))
336+
337+
def test_low_rank_decomposition_raises_for_spin_exchange(self):
338+
"""Non-spin-symmetric input raises ValueError with informative message."""
339+
two_body_tensor = self._make_spin_exchange_tensor()
340+
with self.assertRaises(ValueError) as ctx:
341+
low_rank_two_body_decomposition(two_body_tensor, spin_basis=True)
342+
self.assertIn('spin-symmetric', str(ctx.exception))
343+
344+
def test_spin_symmetric_hamiltonian_succeeds(self):
345+
"""Spin-symmetric Hamiltonians decompose without error."""
346+
filename = os.path.join(DATA_DIRECTORY, 'H2_sto-3g_singlet_0.7414')
347+
molecule = MolecularData(filename=filename)
348+
two_body_coefficients = molecule.get_molecular_hamiltonian().two_body_tensor
349+
_, _ = get_chemist_two_body_coefficients(two_body_coefficients, spin_basis=True)
350+
eigenvalues, _, _, _ = low_rank_two_body_decomposition(
351+
two_body_coefficients, spin_basis=True
352+
)
353+
self.assertGreater(len(eigenvalues), 0)

src/openfermion/circuits/trotter/algorithms/low_rank.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ class LowRankTrotterAlgorithm(TrotterAlgorithm):
6464
or it is chosen so that
6565
$\sum_{l=0}^{L-1} (\sum_{pq} |g_{lpq}|)^2 |\lambda_l| < x$
6666
where x is a truncation threshold specified by user.
67+
68+
Note:
69+
When ``spin_basis=True`` (the default), the input
70+
:class:`~openfermion.ops.InteractionOperator` must have a
71+
spin-symmetric two-body tensor, i.e. identical interaction
72+
coefficients for the alpha and beta spin channels. Hamiltonians
73+
that break this symmetry will raise a ``ValueError``.
6774
"""
6875

6976
supported_types = {ops.InteractionOperator}

src/openfermion/circuits/trotter/simulate_trotter_test.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,3 +499,19 @@ def test_trotter_misspecified_control_raises_error(algorithm_type, hamiltonian):
499499
next(algorithm.trotter_step(qubits, time))
500500
with pytest.raises(TypeError):
501501
next(algorithm.trotter_step(qubits, time, control_qubit=2))
502+
503+
504+
def test_simulate_trotter_spin_exchange_raises_value_error():
505+
"""LOW_RANK raises ValueError for a non-spin-symmetric Hamiltonian."""
506+
h_sf = openfermion.FermionOperator('0^ 3^ 1 2', 1.0) + openfermion.FermionOperator(
507+
'2^ 1^ 3 0', 1.0
508+
)
509+
interaction_hamiltonian = openfermion.get_interaction_operator(h_sf, n_qubits=4)
510+
qubits = cirq.LineQubit.range(4)
511+
512+
with pytest.raises(ValueError, match='spin-symmetric'):
513+
next(
514+
simulate_trotter(
515+
qubits=qubits, hamiltonian=interaction_hamiltonian, time=1.0, algorithm=LOW_RANK
516+
)
517+
)

0 commit comments

Comments
 (0)