diff --git a/qiskit/synthesis/evolution/product_formula.py b/qiskit/synthesis/evolution/product_formula.py index b314d5ec5e62..980d8cdfefd9 100644 --- a/qiskit/synthesis/evolution/product_formula.py +++ b/qiskit/synthesis/evolution/product_formula.py @@ -191,12 +191,6 @@ def settings(self) -> dict[str, typing.Any]: "preserve_order": self.preserve_order, } - def _normalize_coefficients( - self, paulis: list[str | list[int], float | complex | ParameterExpression] - ) -> list[str | list[int] | ParameterValueType]: - """Ensure the coefficients are real (or parameter expressions).""" - return [[(op, qubits, real_or_fail(coeff)) for op, qubits, coeff in ops] for ops in paulis] - def _custom_evolution(self, num_qubits, pauli_rotations): """Implement the evolution for the non-standard path. diff --git a/qiskit/synthesis/evolution/suzuki_trotter.py b/qiskit/synthesis/evolution/suzuki_trotter.py index 209f377351a7..8e2835f505b9 100644 --- a/qiskit/synthesis/evolution/suzuki_trotter.py +++ b/qiskit/synthesis/evolution/suzuki_trotter.py @@ -18,7 +18,9 @@ import typing from collections.abc import Callable from itertools import chain +import numpy as np +from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.quantum_info.operators import SparsePauliOp, Pauli from qiskit.utils.deprecation import deprecate_arg @@ -157,7 +159,10 @@ def expand( time = evolution.time def to_sparse_list(operator): - paulis = (time * (2 / self.reps) * operator).to_sparse_list() + paulis = [ + (pauli, indices, real_or_fail(coeff) * time * 2 / self.reps) + for pauli, indices, coeff in operator.to_sparse_list() + ] if not self.preserve_order: return reorder_paulis(paulis) @@ -171,9 +176,6 @@ def to_sparse_list(operator): # here would be the location to do so. non_commuting = [[op] for op in to_sparse_list(operators)] - # normalize coefficients, i.e. ensure they are float or ParameterExpression - non_commuting = self._normalize_coefficients(non_commuting) - # we're already done here since Lie Trotter does not do any operator repetition product_formula = self._recurse(self.order, non_commuting) flattened = self.reps * list(chain.from_iterable(product_formula)) @@ -213,3 +215,18 @@ def _recurse(order, grouped_paulis): ], ) return outer + inner + outer + + +def real_or_fail(value, tol=100): + """Return real if close, otherwise fail. Unbound parameters are left unchanged. + + Based on NumPy's ``real_if_close``, i.e. ``tol`` is in terms of machine precision for float. + """ + if isinstance(value, ParameterExpression): + return value + + abstol = tol * np.finfo(float).eps + if abs(np.imag(value)) < abstol: + return np.real(value) + + raise ValueError(f"Encountered complex value {value}, but expected real.") diff --git a/releasenotes/notes/fix-pauli-sympify-ea9acceb2a923aff.yaml b/releasenotes/notes/fix-pauli-sympify-ea9acceb2a923aff.yaml new file mode 100644 index 000000000000..248a09b6f88e --- /dev/null +++ b/releasenotes/notes/fix-pauli-sympify-ea9acceb2a923aff.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + Fixed an inconsistency in the circuit generated by a Pauli evolution synthesis + with :class:`.SuzukiTrotter` or :class:`.LieTrotter` (the default) method. + For parameterized evolution times, the resulting circuits contained parameters + with a spurious, zero complex part, which affected the output of + :meth:`.ParameterExpression.sympify`. The output now correctly is only real. + Fixed `#13642 `__. diff --git a/test/python/circuit/library/test_evolution_gate.py b/test/python/circuit/library/test_evolution_gate.py index 1b55ea920289..72acfc421a31 100644 --- a/test/python/circuit/library/test_evolution_gate.py +++ b/test/python/circuit/library/test_evolution_gate.py @@ -479,6 +479,20 @@ def atomic_evolution(pauli, time): decomposed = evo_gate.definition.decompose() self.assertEqual(decomposed.count_ops()["cx"], reps * 3 * 4) + def test_sympify_is_real(self): + """Test converting the parameters to sympy is real. + + Regression test of #13642, where the parameters in the Pauli evolution had a spurious + zero complex part. Even though this is not noticable upon binding or printing the parameter, + it does affect the output of Parameter.sympify. + """ + time = Parameter("t") + evo = PauliEvolutionGate(Z, time=time) + + angle = evo.definition.data[0].operation.params[0] + expected = (2.0 * time).sympify() + self.assertEqual(expected, angle.sympify()) + def exact_atomic_evolution(circuit, pauli, time): """An exact atomic evolution for Suzuki-Trotter.