Skip to content

Commit

Permalink
fix the 0 complex part (#13643)
Browse files Browse the repository at this point in the history
Co-authored-by: Elena Peña Tapia <[email protected]>
  • Loading branch information
Cryoris and ElePT authored Jan 10, 2025
1 parent b872e88 commit 2476188
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 10 deletions.
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

0 comments on commit 2476188

Please sign in to comment.