Skip to content

fix: handle optional SymPy support and zero-pruning in SymbolicOperat… - #1452

Open
anishbhujbal7 wants to merge 4 commits into
quantumlib:mainfrom
anishbhujbal7:main
Open

fix: handle optional SymPy support and zero-pruning in SymbolicOperat…#1452
anishbhujbal7 wants to merge 4 commits into
quantumlib:mainfrom
anishbhujbal7:main

Conversation

@anishbhujbal7

Copy link
Copy Markdown

Summary of Changes

This PR fixes issues in SymbolicOperator and QubitOperator when using SymPy expressions as coefficients, specifically regarding zero-pruning, tolerance filtering, and runtime dependency safety.

  1. Dependency Guarding (HAS_SYMPY):

    • Wrapped direct sympy references with global HAS_SYMPY guards to ensure minimal environments without sympy installed do not encounter runtime crashes during import or operations.
  2. Improved Zero-Pruning & Tolerance Filtering in compress():

    • Refactored compress() to check simplified_coeff.is_zero and handle relational/symbolic comparisons cleanly.
    • Updated tolerance evaluation (abs_tol) using simplified_coeff.is_number so symbolic expressions that evaluate to small numerical values are pruned as expected without breaking non-numeric symbolic terms.
  3. Symbolic Equivalence Testing:

    • Updated equality assertions in test suites (qubit_operator_test.py and symbolic_operator_test.py) to evaluate symbolic expressions using sympy.simplify() rather than relying on strict == identity, preventing false assertion failures on equivalent expressions like x vs 1.0*x.

Issue Addressed

Closes #1053


Testing & Verification

  • Ran unit tests covering SymbolicOperator and QubitOperator with SymPy enabled and disabled.
  • Full repository test suite executed locally (pytest src/ -s --ignore=src/openfermion/testing/examples_test.py):

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces optional SymPy support for symbolic operators, allowing SymPy expressions, symbols, and basic types to be used as coefficients. It refactors the compress method to handle symbolic expressions and adds corresponding unit tests. The review feedback highlights a regression in the refactored compress method where small imaginary and real parts of SymPy expressions are no longer pruned, which could affect numerical stability and hermiticity. To address this, it is recommended to simplify the coefficients first and then apply the original pruning logic, as well as add a test case to verify this behavior.

Comment on lines +713 to 732
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

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.

Comment on lines +1475 to +1483
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)

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)

@anishbhujbal7

Copy link
Copy Markdown
Author

Hi maintainers!

The CI pytest step failed due to 100% diff-coverage enforcement on 3 modified lines in src/openfermion/ops/operators/symbolic_operator.py:

  • Line 35: COEFFICIENT_TYPES = ... (fallback branch when sympy is missing)
  • Lines 724 & 731: continue and except (TypeError, ValueError): exception handling branches.

All tests are passing (2238 passed), but these specific fallback/error handling branches were not triggered during test execution. Should I add dedicated unit tests (e.g., mocking missing sympy and triggering the type error exception) to cover these lines, or apply # pragma: no cover to the fallback paths?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Are there any plans to support symbolic coefficients for QubitOperator?

1 participant