Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/openfermion/ops/operators/qubit_operator_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,21 @@ def test_get_operator_groups_six():

assert check_length(operator_groups, [4, 4, 3, 3, 3, 3])
assert check_sum(operator_groups, operator)


def test_qubit_operator_sympy_support():
import sympy

x = sympy.Symbol('x')
y = sympy.Symbol('y')
# Hamiltonian creation as described in issue #1053
hamiltonian = x * QubitOperator('X0 X5') + 0.3 * QubitOperator('Z0')

# Check symbolic equality using sympy.simplify
term_coeff = hamiltonian.terms[((0, 'X'), (5, 'X'))]
assert sympy.simplify(term_coeff - x) == 0
assert hamiltonian.terms[((0, 'Z'),)] == 0.3

cancelled = hamiltonian - x * QubitOperator('X0 X5')
cancelled.compress()
assert ((0, 'X'), (5, 'X')) not in cancelled.terms
55 changes: 38 additions & 17 deletions src/openfermion/ops/operators/symbolic_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,25 @@
import re
import warnings
import numbers

import sympy
from typing import Tuple, Type

from openfermion.config import EQ_TOLERANCE

COEFFICIENT_TYPES = (int, float, complex, sympy.Expr, numbers.Number)
try:
import sympy

HAS_SYMPY = True
except ImportError: # pragma: no cover
HAS_SYMPY = False
sympy = None

COEFFICIENT_TYPES: Tuple[Type, ...]
if HAS_SYMPY:
COEFFICIENT_TYPES = (int, float, complex, numbers.Number, sympy.Expr, sympy.Symbol, sympy.Basic)
else:
COEFFICIENT_TYPES = (int, float, complex, numbers.Number)

# COEFFICIENT_TYPES = (int, float, complex, sympy.Expr, numbers.Number)/


class SymbolicOperator(metaclass=abc.ABCMeta):
Expand Down Expand Up @@ -692,33 +705,41 @@ def isclose(self, other, tol=None, rtol=EQ_TOLERANCE, atol=EQ_TOLERANCE):
return True

def compress(self, abs_tol=EQ_TOLERANCE):
"""
Eliminates all terms with coefficients close to zero and removes
"""Eliminates all terms with coefficients close to zero and removes
small imaginary and real parts.

Args:
abs_tol(float): Absolute tolerance, must be at least 0.0
"""
new_terms = {}
for term in self.terms:
coeff = self.terms[term]

if isinstance(coeff, sympy.Expr):
if sympy.simplify(sympy.im(coeff) <= abs_tol) == True:
coeff = sympy.re(coeff)
if sympy.simplify(sympy.re(coeff) <= abs_tol) == True:
coeff = 1j * sympy.im(coeff)
if sympy.simplify(abs(coeff) <= abs_tol) != True:
new_terms[term] = coeff
for term, coeff in self.terms.items():
if HAS_SYMPY and isinstance(coeff, (sympy.Expr, sympy.Symbol, sympy.Basic)):
# SymPy symbolic handling
if coeff == 0 or coeff.is_zero is True:
continue

# Simplify the symbolic expression
simplified_coeff = sympy.simplify(coeff)
if simplified_coeff == 0 or simplified_coeff.is_zero is True:
continue

# Check if simplified expression evaluates to a float/number under abs_tol
if simplified_coeff.is_number:
try:
if abs(complex(simplified_coeff)) <= abs_tol:
continue
except (TypeError, ValueError):
pass

new_terms[term] = simplified_coeff
continue
Comment on lines +716 to 735

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The refactored compress method introduces a regression where small imaginary and real parts of SymPy expressions (both symbolic and numeric) are no longer pruned. The original implementation used sympy.simplify(sympy.im(coeff) <= abs_tol) == True to discard negligible imaginary/real parts, which is crucial for maintaining hermiticity and numerical stability in downstream calculations.

By simplifying the coefficient first and then applying the original pruning logic, we can fix the x - x zero-pruning issue while preserving the real/imaginary part pruning.

Suggested change
if HAS_SYMPY and isinstance(coeff, (sympy.Expr, sympy.Symbol, sympy.Basic)):
# SymPy symbolic handling
if coeff == 0 or coeff.is_zero is True:
continue
# Simplify the symbolic expression
simplified_coeff = sympy.simplify(coeff)
if simplified_coeff == 0 or simplified_coeff.is_zero is True:
continue
# Check if simplified expression evaluates to a float/number under abs_tol
if simplified_coeff.is_number:
try:
if abs(complex(simplified_coeff)) <= abs_tol:
continue
except (TypeError, ValueError):
pass
new_terms[term] = simplified_coeff
continue
if HAS_SYMPY and isinstance(coeff, sympy.Basic):
simplified_coeff = sympy.simplify(coeff)
if sympy.simplify(sympy.im(simplified_coeff) <= abs_tol) == True:
simplified_coeff = sympy.re(simplified_coeff)
if sympy.simplify(sympy.re(simplified_coeff) <= abs_tol) == True:
simplified_coeff = 1j * sympy.im(simplified_coeff)
if sympy.simplify(abs(simplified_coeff) <= abs_tol) != True:
new_terms[term] = simplified_coeff
continue
References
  1. When refactoring numerical accumulation or operator transforms, preserve the existing multi-step thresholding/dropping logic (e.g., double thresholding) if bit-for-bit identical output is required to avoid breaking tight tests.


# Remove small imaginary and real parts
# Standard numerical handling
if abs(coeff.imag) <= abs_tol:
coeff = coeff.real
if abs(coeff.real) <= abs_tol:
coeff = 1.0j * coeff.imag

# Add the term if the coefficient is large enough
if abs(coeff) > abs_tol:
new_terms[term] = coeff

Expand Down
34 changes: 34 additions & 0 deletions src/openfermion/ops/operators/symbolic_operator_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1447,3 +1447,37 @@ def test_many_body_order_sympy(self):
def test_tracenorm_zero(self):
op = MockOperator2()
self.assertFalse(op.induced_norm())

def test_symbolic_operator_sympy_coefficients(self):
import sympy

x = sympy.Symbol('x')
y = sympy.Symbol('y')

# 1. Initialize MockOperator1 with a SymPy symbol coefficient
op1 = MockOperator1(((0, 1), (1, 0)), x)
self.assertEqual(op1.terms[((0, 1), (1, 0))], x)

# 2. Scalar multiplication with a SymPy expression
op2 = op1 * (2 * y)
self.assertEqual(op2.terms[((0, 1), (1, 0))], 2 * x * y)

# 3. Addition of symbolic operators
op3 = MockOperator1(((0, 1), (1, 0)), y)
op_sum = op1 + op3
self.assertEqual(op_sum.terms[((0, 1), (1, 0))], x + y)

# 4. Symbolic zero cancellation (x - x -> 0 term should be pruned)
op4 = MockOperator1(((0, 1), (1, 0)), -x)
op_cancel = op1 + op4
self.assertEqual(len(op_cancel.terms), 0)

def test_compress_sympy_coefficients(self):
import sympy

x = sympy.Symbol('x')

# Operator with x - x (evaluates to 0 on compress)
op = MockOperator1(((0, 1), (1, 0)), x - x)
op.compress()
self.assertEqual(len(op.terms), 0)
Comment on lines +1475 to +1483

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

To prevent future regressions where small imaginary or real parts of symbolic expressions are not pruned during compression, we should add a test case that specifically verifies this behavior.

Suggested change
def test_compress_sympy_coefficients(self):
import sympy
x = sympy.Symbol('x')
# Operator with x - x (evaluates to 0 on compress)
op = MockOperator1(((0, 1), (1, 0)), x - x)
op.compress()
self.assertEqual(len(op.terms), 0)
def test_compress_sympy_coefficients(self):
import sympy
x = sympy.Symbol('x')
# Operator with x - x (evaluates to 0 on compress)
op = MockOperator1(((0, 1), (1, 0)), x - x)
op.compress()
self.assertEqual(len(op.terms), 0)
# Operator with x + 1e-15j (imaginary part should be pruned if x is real)
x_real = sympy.Symbol('x', real=True)
op2 = MockOperator1(((0, 1), (1, 0)), x_real + 1e-15j)
op2.compress()
self.assertEqual(op2.terms[((0, 1), (1, 0))], x_real)

Loading