Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix spurious 0 complex part in Pauli evolution (backport #13643) #13646

Merged
merged 1 commit into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 0 additions & 6 deletions qiskit/synthesis/evolution/product_formula.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
25 changes: 21 additions & 4 deletions qiskit/synthesis/evolution/suzuki_trotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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))
Expand Down Expand Up @@ -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.")
9 changes: 9 additions & 0 deletions releasenotes/notes/fix-pauli-sympify-ea9acceb2a923aff.yaml
Original file line number Diff line number Diff line change
@@ -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 <https://github.com/Qiskit/qiskit/pull/13642>`__.
14 changes: 14 additions & 0 deletions test/python/circuit/library/test_evolution_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading