From 6258eb644214bf33e12664dfb538e280f4c86b59 Mon Sep 17 00:00:00 2001 From: anishbhujbal7 Date: Tue, 8 Sep 2026 16:39:04 +0000 Subject: [PATCH 1/4] fix: handle optional SymPy support and zero-pruning in SymbolicOperator (#1053) --- .../ops/operators/qubit_operator_test.py | 19 +++++++ .../ops/operators/symbolic_operator.py | 53 +++++++++++++------ .../ops/operators/symbolic_operator_test.py | 34 ++++++++++++ 3 files changed, 89 insertions(+), 17 deletions(-) diff --git a/src/openfermion/ops/operators/qubit_operator_test.py b/src/openfermion/ops/operators/qubit_operator_test.py index 56ba4128a..eb3485ccf 100644 --- a/src/openfermion/ops/operators/qubit_operator_test.py +++ b/src/openfermion/ops/operators/qubit_operator_test.py @@ -279,3 +279,22 @@ 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 + + diff --git a/src/openfermion/ops/operators/symbolic_operator.py b/src/openfermion/ops/operators/symbolic_operator.py index 613930160..91589aa1b 100644 --- a/src/openfermion/ops/operators/symbolic_operator.py +++ b/src/openfermion/ops/operators/symbolic_operator.py @@ -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): @@ -692,38 +702,47 @@ 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 - # 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. @@ -794,4 +813,4 @@ def get_operator_groups(self, num_groups): for i in range(num_groups): yield self.accumulate( itertools.islice(operators, len(range(i, len(self.terms), num_groups))) - ) + ) \ No newline at end of file diff --git a/src/openfermion/ops/operators/symbolic_operator_test.py b/src/openfermion/ops/operators/symbolic_operator_test.py index ea914f607..8815de56a 100644 --- a/src/openfermion/ops/operators/symbolic_operator_test.py +++ b/src/openfermion/ops/operators/symbolic_operator_test.py @@ -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) From 1e102ccf5439a756a329493b3ca7df267cf2b9b7 Mon Sep 17 00:00:00 2001 From: anishbhujbal7 Date: Tue, 8 Sep 2026 17:11:23 +0000 Subject: [PATCH 2/4] fix(symbolic_operator): resolve linting errors and import order --- src/openfermion/ops/operators/qubit_operator_test.py | 3 +-- src/openfermion/ops/operators/symbolic_operator.py | 7 ++++--- src/openfermion/ops/operators/symbolic_operator_test.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/openfermion/ops/operators/qubit_operator_test.py b/src/openfermion/ops/operators/qubit_operator_test.py index eb3485ccf..fb7335b22 100644 --- a/src/openfermion/ops/operators/qubit_operator_test.py +++ b/src/openfermion/ops/operators/qubit_operator_test.py @@ -280,6 +280,7 @@ 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 @@ -296,5 +297,3 @@ def test_qubit_operator_sympy_support(): cancelled = hamiltonian - x * QubitOperator('X0 X5') cancelled.compress() assert ((0, 'X'), (5, 'X')) not in cancelled.terms - - diff --git a/src/openfermion/ops/operators/symbolic_operator.py b/src/openfermion/ops/operators/symbolic_operator.py index 91589aa1b..ff8dd92d4 100644 --- a/src/openfermion/ops/operators/symbolic_operator.py +++ b/src/openfermion/ops/operators/symbolic_operator.py @@ -17,8 +17,11 @@ import warnings import numbers +from openfermion.config import EQ_TOLERANCE + try: import sympy + HAS_SYMPY = True except ImportError: # pragma: no cover HAS_SYMPY = False @@ -29,7 +32,6 @@ else: COEFFICIENT_TYPES = (int, float, complex, numbers.Number) -from openfermion.config import EQ_TOLERANCE # COEFFICIENT_TYPES = (int, float, complex, sympy.Expr, numbers.Number)/ @@ -742,7 +744,6 @@ def compress(self, abs_tol=EQ_TOLERANCE): self.terms = new_terms - def induced_norm(self, order=1): r""" Compute the induced p-norm of the operator. @@ -813,4 +814,4 @@ def get_operator_groups(self, num_groups): for i in range(num_groups): yield self.accumulate( itertools.islice(operators, len(range(i, len(self.terms), num_groups))) - ) \ No newline at end of file + ) diff --git a/src/openfermion/ops/operators/symbolic_operator_test.py b/src/openfermion/ops/operators/symbolic_operator_test.py index 8815de56a..953b35e6b 100644 --- a/src/openfermion/ops/operators/symbolic_operator_test.py +++ b/src/openfermion/ops/operators/symbolic_operator_test.py @@ -1476,7 +1476,7 @@ 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() From 65877efc1cb423086db4085e8a202bbe8b8ff252 Mon Sep 17 00:00:00 2001 From: anishbhujbal7 Date: Tue, 8 Sep 2026 17:32:37 +0000 Subject: [PATCH 3/4] style: apply black formatting and add mypy type annotation for COEFFICIENT_TYPES --- src/openfermion/ops/operators/symbolic_operator.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/openfermion/ops/operators/symbolic_operator.py b/src/openfermion/ops/operators/symbolic_operator.py index ff8dd92d4..dd3dbae87 100644 --- a/src/openfermion/ops/operators/symbolic_operator.py +++ b/src/openfermion/ops/operators/symbolic_operator.py @@ -16,9 +16,13 @@ import re import warnings import numbers +from typing import Tuple, Type from openfermion.config import EQ_TOLERANCE + + + try: import sympy @@ -27,12 +31,12 @@ 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)/ From 7e9d104fe7998e6f57cb2f78ace7e071d7691255 Mon Sep 17 00:00:00 2001 From: anishbhujbal7 Date: Tue, 8 Sep 2026 17:41:08 +0000 Subject: [PATCH 4/4] style: apply black formatting and add mypy annotation for COEFFICIENT_TYPES --- src/openfermion/ops/operators/symbolic_operator.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/openfermion/ops/operators/symbolic_operator.py b/src/openfermion/ops/operators/symbolic_operator.py index dd3dbae87..90d783dbb 100644 --- a/src/openfermion/ops/operators/symbolic_operator.py +++ b/src/openfermion/ops/operators/symbolic_operator.py @@ -20,9 +20,6 @@ from openfermion.config import EQ_TOLERANCE - - - try: import sympy