From 26a433280f64a3cfbf15153eca0a4c2940b2ad28 Mon Sep 17 00:00:00 2001 From: rosspeili Date: Sat, 2 May 2026 21:04:27 +0300 Subject: [PATCH 1/4] 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. --- src/openfermion/circuits/low_rank.py | 48 +++++++++++++++++-- src/openfermion/circuits/low_rank_test.py | 40 +++++++++++++++- .../circuits/trotter/algorithms/low_rank.py | 7 +++ .../circuits/trotter/simulate_trotter_test.py | 16 +++++++ 4 files changed, 104 insertions(+), 7 deletions(-) diff --git a/src/openfermion/circuits/low_rank.py b/src/openfermion/circuits/low_rank.py index 330c4556d..0d075afe0 100644 --- a/src/openfermion/circuits/low_rank.py +++ b/src/openfermion/circuits/low_rank.py @@ -44,8 +44,13 @@ def get_chemist_two_body_coefficients(two_body_coefficients, spin_basis=True): giving the $g_{pqrs}$ tensor in chemist notation. Raises: - TypeError: Input must be two-body number conserving - FermionOperator or InteractionOperator. + ValueError: Input two-body tensor is not spin-symmetric. The LOW_RANK + decomposition requires a spin-symmetric interaction when + ``spin_basis=True``. A spin-symmetric interaction satisfies + ``h[p,q,r,s] == h[p+1,q+1,r+1,s+1]`` for all even p, q, r, s + (i.e., the same coefficient for alpha and beta spin channels). + Consider passing ``spin_basis=False`` if your Hamiltonian is not + spin-symmetric. """ # Initialize. n_orbitals = two_body_coefficients.shape[0] @@ -57,10 +62,34 @@ def get_chemist_two_body_coefficients(two_body_coefficients, spin_basis=True): n_orbitals = n_orbitals // 2 alpha_indices = list(range(0, n_orbitals * 2, 2)) beta_indices = list(range(1, n_orbitals * 2, 2)) - chemist_two_body_coefficients = chemist_two_body_coefficients[ + # Extract the αα→ββ block, which is the only block used by the + # spatial-orbital low-rank decomposition. + alpha_alpha_beta_beta = chemist_two_body_coefficients[ numpy.ix_(alpha_indices, alpha_indices, beta_indices, beta_indices) ] + # Validate spin-symmetry by checking that the extracted block is + # symmetric when reshaped to a matrix. For a spin-symmetric interaction + # the chemist tensor satisfies g[p,q,r,s] == g[r,s,p,q] (up to + # permutation symmetry), which makes the reshaped matrix symmetric. + # General spin-dependent interactions such as spin-exchange Hamiltonians + # produce an asymmetric matrix here and cannot be handled by this + # spatial-orbital downfolding approach. + flat = numpy.reshape(alpha_alpha_beta_beta, (n_orbitals**2, n_orbitals**2)) + spin_asymmetry = numpy.sum(numpy.absolute(flat - flat.T)) + if spin_asymmetry > EQ_TOLERANCE: + raise ValueError( + 'The two-body tensor is not spin-symmetric. The LOW_RANK ' + 'decomposition requires a spin-symmetric interaction when ' + 'spin_basis=True (i.e., the same coefficients for alpha and ' + 'beta spin channels). Spin-dependent interactions such as ' + 'spin-exchange Hamiltonians violate this requirement. ' + 'Consider passing spin_basis=False if your Hamiltonian is ' + 'not spin-symmetric.' + ) + + chemist_two_body_coefficients = alpha_alpha_beta_beta + # Determine a one body correction in the spin basis from spatial basis. one_body_correction = numpy.zeros((2 * n_orbitals, 2 * n_orbitals), complex) for p, q, r, s in itertools.product(range(n_orbitals), repeat=4): @@ -106,7 +135,11 @@ def low_rank_two_body_decomposition( $\sum_{l=0}^{L-1} (\sum_{pq} |g_{lpq}|)^2 |\lambda_l| < x$ Raises: - TypeError: Invalid two-body coefficient tensor specification. + ValueError: The two-body tensor failed symmetry or reality checks + required for the low-rank decomposition. When ``spin_basis=True``, + the tensor must be spin-symmetric (see + :func:`get_chemist_two_body_coefficients`). When ``spin_basis=False``, + the chemist-ordered tensor must be real and symmetric. """ # Initialize N^2 by N^2 interaction array. one_body_correction, chemist_two_body_coefficients = get_chemist_two_body_coefficients( @@ -120,7 +153,12 @@ def low_rank_two_body_decomposition( asymmetry = numpy.sum(numpy.absolute(interaction_array - interaction_array.transpose())) imaginary_norm = numpy.sum(numpy.absolute(interaction_array.imag)) if asymmetry > EQ_TOLERANCE or imaginary_norm > EQ_TOLERANCE: - raise TypeError('Invalid two-body coefficient tensor specification.') + raise ValueError( + 'The two-body coefficient tensor failed the symmetry or reality ' + 'checks required by the low-rank decomposition. If spin_basis=True, ' + 'ensure the Hamiltonian is spin-symmetric. If spin_basis=False, ' + 'ensure the chemist-ordered two-body tensor is real and symmetric.' + ) # Decompose with exact diagonalization. eigenvalues, eigenvectors = numpy.linalg.eigh(interaction_array) diff --git a/src/openfermion/circuits/low_rank_test.py b/src/openfermion/circuits/low_rank_test.py index 89503c1de..2e79ad1c8 100644 --- a/src/openfermion/circuits/low_rank_test.py +++ b/src/openfermion/circuits/low_rank_test.py @@ -163,8 +163,8 @@ def test_molecular_operator_consistency(self): ) self.assertAlmostEqual(trunc_error, 0.0) - # Check for property errors - with self.assertRaises(TypeError): + # Check for property errors — imaginary tensor should raise ValueError + with self.assertRaises(ValueError): eigenvalues, one_body_squares, _, trunc_error = low_rank_two_body_decomposition( two_body_coefficients + 0.01j, truncation_threshold=1.0, final_rank=1 ) @@ -315,3 +315,39 @@ def test_one_body_squared_nonhermitian_raises_error(self): one_body_matrix = numpy.array([[0, 1], [0, 0]]) with self.assertRaises(ValueError): prepare_one_body_squared_evolution(one_body_matrix, spin_basis=False) + + +class SpinExchangeTest(unittest.TestCase): + """Tests for spin-symmetry validation in the low-rank decomposition.""" + + def _make_spin_exchange_tensor(self): + """Return the two_body_tensor for H = a^dag_0 a^dag_3 a_1 a_2 + h.c.""" + from openfermion import get_interaction_operator + + h_sf = FermionOperator('0^ 3^ 1 2', 1.0) + FermionOperator('2^ 1^ 3 0', 1.0) + return get_interaction_operator(h_sf, n_qubits=4).two_body_tensor + + def test_get_chemist_two_body_coefficients_raises_for_spin_exchange(self): + """Non-spin-symmetric input raises ValueError with informative message.""" + two_body_tensor = self._make_spin_exchange_tensor() + with self.assertRaises(ValueError) as ctx: + get_chemist_two_body_coefficients(two_body_tensor, spin_basis=True) + self.assertIn('spin-symmetric', str(ctx.exception)) + + def test_low_rank_decomposition_raises_for_spin_exchange(self): + """Non-spin-symmetric input raises ValueError with informative message.""" + two_body_tensor = self._make_spin_exchange_tensor() + with self.assertRaises(ValueError) as ctx: + low_rank_two_body_decomposition(two_body_tensor, spin_basis=True) + self.assertIn('spin-symmetric', str(ctx.exception)) + + def test_spin_symmetric_hamiltonian_succeeds(self): + """Spin-symmetric Hamiltonians decompose without error.""" + filename = os.path.join(DATA_DIRECTORY, 'H2_sto-3g_singlet_0.7414') + molecule = MolecularData(filename=filename) + two_body_coefficients = molecule.get_molecular_hamiltonian().two_body_tensor + _, _ = get_chemist_two_body_coefficients(two_body_coefficients, spin_basis=True) + eigenvalues, _, _, _ = low_rank_two_body_decomposition( + two_body_coefficients, spin_basis=True + ) + self.assertGreater(len(eigenvalues), 0) diff --git a/src/openfermion/circuits/trotter/algorithms/low_rank.py b/src/openfermion/circuits/trotter/algorithms/low_rank.py index 3cd150d5c..26bf2e165 100644 --- a/src/openfermion/circuits/trotter/algorithms/low_rank.py +++ b/src/openfermion/circuits/trotter/algorithms/low_rank.py @@ -64,6 +64,13 @@ class LowRankTrotterAlgorithm(TrotterAlgorithm): or it is chosen so that $\sum_{l=0}^{L-1} (\sum_{pq} |g_{lpq}|)^2 |\lambda_l| < x$ where x is a truncation threshold specified by user. + + Note: + When ``spin_basis=True`` (the default), the input + :class:`~openfermion.ops.InteractionOperator` must have a + spin-symmetric two-body tensor, i.e. identical interaction + coefficients for the alpha and beta spin channels. Hamiltonians + that break this symmetry will raise a ``ValueError``. """ supported_types = {ops.InteractionOperator} diff --git a/src/openfermion/circuits/trotter/simulate_trotter_test.py b/src/openfermion/circuits/trotter/simulate_trotter_test.py index ab5365f7b..7db63f4fe 100644 --- a/src/openfermion/circuits/trotter/simulate_trotter_test.py +++ b/src/openfermion/circuits/trotter/simulate_trotter_test.py @@ -499,3 +499,19 @@ def test_trotter_misspecified_control_raises_error(algorithm_type, hamiltonian): next(algorithm.trotter_step(qubits, time)) with pytest.raises(TypeError): next(algorithm.trotter_step(qubits, time, control_qubit=2)) + + +def test_simulate_trotter_spin_exchange_raises_value_error(): + """LOW_RANK raises ValueError for a non-spin-symmetric Hamiltonian.""" + h_sf = openfermion.FermionOperator('0^ 3^ 1 2', 1.0) + openfermion.FermionOperator( + '2^ 1^ 3 0', 1.0 + ) + interaction_hamiltonian = openfermion.get_interaction_operator(h_sf, n_qubits=4) + qubits = cirq.LineQubit.range(4) + + with pytest.raises(ValueError, match='spin-symmetric'): + next( + simulate_trotter( + qubits=qubits, hamiltonian=interaction_hamiltonian, time=1.0, algorithm=LOW_RANK + ) + ) From b3760699aec71f969a7aed6622b81400e37dddbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=84=E3=83=B3=E3=83=87=E3=83=AC?= Date: Sat, 2 May 2026 21:20:11 +0300 Subject: [PATCH 2/4] Update src/openfermion/circuits/low_rank.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/openfermion/circuits/low_rank.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openfermion/circuits/low_rank.py b/src/openfermion/circuits/low_rank.py index 0d075afe0..48da94eba 100644 --- a/src/openfermion/circuits/low_rank.py +++ b/src/openfermion/circuits/low_rank.py @@ -76,7 +76,7 @@ def get_chemist_two_body_coefficients(two_body_coefficients, spin_basis=True): # produce an asymmetric matrix here and cannot be handled by this # spatial-orbital downfolding approach. flat = numpy.reshape(alpha_alpha_beta_beta, (n_orbitals**2, n_orbitals**2)) - spin_asymmetry = numpy.sum(numpy.absolute(flat - flat.T)) + spin_asymmetry = numpy.amax(numpy.absolute(flat - flat.T)) if spin_asymmetry > EQ_TOLERANCE: raise ValueError( 'The two-body tensor is not spin-symmetric. The LOW_RANK ' From aed3bd86665329cf9873efdb8b77a235f9a723c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=84=E3=83=B3=E3=83=87=E3=83=AC?= Date: Sat, 2 May 2026 21:20:22 +0300 Subject: [PATCH 3/4] Update src/openfermion/circuits/low_rank.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/openfermion/circuits/low_rank.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/openfermion/circuits/low_rank.py b/src/openfermion/circuits/low_rank.py index 48da94eba..1b2ddfee0 100644 --- a/src/openfermion/circuits/low_rank.py +++ b/src/openfermion/circuits/low_rank.py @@ -150,8 +150,8 @@ def low_rank_two_body_decomposition( interaction_array = numpy.reshape(chemist_two_body_coefficients, (full_rank, full_rank)) # Make sure interaction array is symmetric and real. - asymmetry = numpy.sum(numpy.absolute(interaction_array - interaction_array.transpose())) - imaginary_norm = numpy.sum(numpy.absolute(interaction_array.imag)) + asymmetry = numpy.amax(numpy.absolute(interaction_array - interaction_array.transpose())) + imaginary_norm = numpy.amax(numpy.absolute(interaction_array.imag)) if asymmetry > EQ_TOLERANCE or imaginary_norm > EQ_TOLERANCE: raise ValueError( 'The two-body coefficient tensor failed the symmetry or reality ' From 37eac9fc444db53ee4d1bed52047cb2bccdc0e38 Mon Sep 17 00:00:00 2001 From: rosspeili Date: Wed, 6 May 2026 11:10:07 +0300 Subject: [PATCH 4/4] docs: convert docstrings from rST to Google style Replace rST-syntax double-backtick inline code (`code`) and cross-reference roles (:func:, :class:) with plain text, per OpenFermion's Google-style (TensorFlow) docstring convention. Affected sections: - get_chemist_two_body_coefficients Raises block - low_rank_two_body_decomposition Raises block - LowRankTrotterAlgorithm Note block --- src/openfermion/circuits/low_rank.py | 10 +++++----- .../circuits/trotter/algorithms/low_rank.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/openfermion/circuits/low_rank.py b/src/openfermion/circuits/low_rank.py index 1b2ddfee0..34706a15f 100644 --- a/src/openfermion/circuits/low_rank.py +++ b/src/openfermion/circuits/low_rank.py @@ -46,10 +46,10 @@ def get_chemist_two_body_coefficients(two_body_coefficients, spin_basis=True): Raises: ValueError: Input two-body tensor is not spin-symmetric. The LOW_RANK decomposition requires a spin-symmetric interaction when - ``spin_basis=True``. A spin-symmetric interaction satisfies - ``h[p,q,r,s] == h[p+1,q+1,r+1,s+1]`` for all even p, q, r, s + spin_basis=True. A spin-symmetric interaction satisfies + h[p,q,r,s] == h[p+1,q+1,r+1,s+1] for all even p, q, r, s (i.e., the same coefficient for alpha and beta spin channels). - Consider passing ``spin_basis=False`` if your Hamiltonian is not + Consider passing spin_basis=False if your Hamiltonian is not spin-symmetric. """ # Initialize. @@ -136,9 +136,9 @@ def low_rank_two_body_decomposition( Raises: ValueError: The two-body tensor failed symmetry or reality checks - required for the low-rank decomposition. When ``spin_basis=True``, + required for the low-rank decomposition. When spin_basis=True, the tensor must be spin-symmetric (see - :func:`get_chemist_two_body_coefficients`). When ``spin_basis=False``, + get_chemist_two_body_coefficients()). When spin_basis=False, the chemist-ordered tensor must be real and symmetric. """ # Initialize N^2 by N^2 interaction array. diff --git a/src/openfermion/circuits/trotter/algorithms/low_rank.py b/src/openfermion/circuits/trotter/algorithms/low_rank.py index 26bf2e165..4cbf2a559 100644 --- a/src/openfermion/circuits/trotter/algorithms/low_rank.py +++ b/src/openfermion/circuits/trotter/algorithms/low_rank.py @@ -66,11 +66,11 @@ class LowRankTrotterAlgorithm(TrotterAlgorithm): where x is a truncation threshold specified by user. Note: - When ``spin_basis=True`` (the default), the input - :class:`~openfermion.ops.InteractionOperator` must have a + When spin_basis=True (the default), the input + InteractionOperator must have a spin-symmetric two-body tensor, i.e. identical interaction coefficients for the alpha and beta spin channels. Hamiltonians - that break this symmetry will raise a ``ValueError``. + that break this symmetry will raise a ValueError. """ supported_types = {ops.InteractionOperator}