Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
19 changes: 19 additions & 0 deletions src/openfermion/ops/operators/qubit_operator_test.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
Expand Down Expand Up @@ -279,3 +279,22 @@

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


53 changes: 36 additions & 17 deletions src/openfermion/ops/operators/symbolic_operator.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
Expand All @@ -17,11 +17,21 @@
import warnings
import numbers

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

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

from openfermion.config import EQ_TOLERANCE

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


class SymbolicOperator(metaclass=abc.ABCMeta):
Expand Down Expand Up @@ -692,38 +702,47 @@
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

self.terms = new_terms


def induced_norm(self, order=1):
r"""
Compute the induced p-norm of the operator.
Expand Down Expand Up @@ -794,4 +813,4 @@
for i in range(num_groups):
yield self.accumulate(
itertools.islice(operators, len(range(i, len(self.terms), num_groups)))
)
)
34 changes: 34 additions & 0 deletions src/openfermion/ops/operators/symbolic_operator_test.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
Expand Down Expand Up @@ -1447,3 +1447,37 @@
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