diff --git a/src/openfermion/ops/operators/qubit_operator_test.py b/src/openfermion/ops/operators/qubit_operator_test.py index 56ba4128a..fb7335b22 100644 --- a/src/openfermion/ops/operators/qubit_operator_test.py +++ b/src/openfermion/ops/operators/qubit_operator_test.py @@ -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 diff --git a/src/openfermion/ops/operators/symbolic_operator.py b/src/openfermion/ops/operators/symbolic_operator.py index 613930160..90d783dbb 100644 --- a/src/openfermion/ops/operators/symbolic_operator.py +++ b/src/openfermion/ops/operators/symbolic_operator.py @@ -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): @@ -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 - # 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 diff --git a/src/openfermion/ops/operators/symbolic_operator_test.py b/src/openfermion/ops/operators/symbolic_operator_test.py index ea914f607..953b35e6b 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)