Skip to content

Commit b98e0d0

Browse files
Fix PauliEvolutionGate (using product formulas) for all-identity Pauli terms (Qiskit#13634)
* fix PauliEvo for all identities * fix rustiq Co-authored-by: Alexander Ivrii <[email protected]> * fix docs * regression test 13644 --------- Co-authored-by: Alexander Ivrii <[email protected]>
1 parent 79f0e72 commit b98e0d0

File tree

7 files changed

+80
-21
lines changed

7 files changed

+80
-21
lines changed

crates/accelerate/src/circuit_library/pauli_evolution.rs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -192,22 +192,22 @@ fn multi_qubit_evolution(
192192
/// followed by a CX-chain and then a single Pauli-Z rotation on the last qubit. Then the CX-chain
193193
/// is uncomputed and the inverse basis transformation applied. E.g. for the evolution under the
194194
/// Pauli string XIYZ we have the circuit
195-
/// ┌───┐┌───────┐┌───┐
196-
/// 0: ─────────────┤ X ├┤ Rz(2) ├┤ X ├──────────
197-
/// ──────┐┌───┐└─┬─┘└───────┘└─┬─┘┌───┐┌────┐
198-
/// 1: ┤ √Xdg ├┤ X ├──■─────────────■──┤ X ├┤ √X ├
199-
/// ──────┘└─┬─┘ └─┬─┘└────┘
200-
/// 2: ─────────────────────────────────┼────────
201-
/// ┌───┐ │ │ ┌───┐
202-
/// 3: ─┤ H ├────■───────────────────────■──┤ H ├─
203-
/// ───┘ └───
195+
///
196+
/// ───┐ ┌───┐┌───────┐┌───┐┌───┐
197+
/// 0: ┤ H ├──────┤ X ├┤ Rz(2) ├┤ X ├┤ H ├────────
198+
/// └───┘ └─┬─┘└───────┘└─┬─┘└───┘
199+
/// 1: ─────────────┼─────────────┼───────────────
200+
/// ────┐┌───┐ │ │ ┌───┐┌──────
201+
/// 2: ┤ √X ├┤ X ├──■─────────────■──┤ X ├┤ √Xdg ├
202+
/// ────┘└─┬─┘ └─┬─┘└──────
203+
/// 3: ────────■───────────────────────■──────────
204204
///
205205
/// Args:
206206
/// num_qubits: The number of qubits in the Hamiltonian.
207207
/// sparse_paulis: The Paulis to implement. Given in a sparse-list format with elements
208-
/// ``(pauli_string, qubit_indices, coefficient)``. An element of the form
209-
/// ``("IXYZ", [0,1,2,3], 0.2)``, for example, is interpreted in terms of qubit indices as
210-
/// I_q0 X_q1 Y_q2 Z_q3 and will use a RZ rotation angle of 0.4.
208+
/// ``(pauli_string, qubit_indices, rz_rotation_angle)``. An element of the form
209+
/// ``("XIYZ", [0,1,2,3], 2)``, for example, is interpreted in terms of qubit indices as
210+
/// X_q0 I_q1 Y_q2 Z_q3 and will use a RZ rotation angle of 2.
211211
/// insert_barriers: If ``true``, insert a barrier in between the evolution of individual
212212
/// Pauli terms.
213213
/// do_fountain: If ``true``, implement the CX propagation as "fountain" shape, where each
@@ -244,7 +244,7 @@ pub fn py_pauli_evolution(
244244
}
245245

246246
paulis.push(pauli);
247-
times.push(time); // note we do not multiply by 2 here, this is done Python side!
247+
times.push(time); // note we do not multiply by 2 here, this is already done Python side!
248248
indices.push(tuple.get_item(1)?.extract::<Vec<u32>>()?)
249249
}
250250

@@ -266,12 +266,12 @@ pub fn py_pauli_evolution(
266266
},
267267
);
268268

269-
// When handling all-identity Paulis above, we added the time as global phase.
270-
// However, the all-identity Paulis should add a negative phase, as they implement
271-
// exp(-i t I). We apply the negative sign here, to only do a single (-1) multiplication,
272-
// instead of doing it every time we find an all-identity Pauli.
269+
// When handling all-identity Paulis above, we added the RZ rotation angle as global phase,
270+
// meaning that we have implemented of exp(i 2t I). However, what we want it to implement
271+
// exp(-i t I). To only use a single multiplication, we apply a factor of -0.5 here.
272+
// This is faster, in particular as long as the parameter expressions are in Python.
273273
if modified_phase {
274-
global_phase = multiply_param(&global_phase, -1.0, py);
274+
global_phase = multiply_param(&global_phase, -0.5, py);
275275
}
276276

277277
CircuitData::from_packed_operations(py, num_qubits as u32, 0, evos, global_phase)

crates/accelerate/src/synthesis/evolution/pauli_network.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ fn inject_rotations(
211211
if pauli_support_size == 0 {
212212
// in case of an all-identity rotation, update global phase by subtracting
213213
// the angle
214-
global_phase = radd_param(global_phase, multiply_param(&angles[i], -1.0, py), py);
214+
global_phase = radd_param(global_phase, multiply_param(&angles[i], -0.5, py), py);
215215
hit_paulis[i] = true;
216216
dag.remove_node(i);
217217
} else if pauli_support_size == 1 && dag.is_front_node(i) {

crates/circuit/src/operations.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2345,8 +2345,14 @@ pub fn add_param(param: &Param, summand: f64, py: Python) -> Param {
23452345
}
23462346

23472347
pub fn radd_param(param1: Param, param2: Param, py: Python) -> Param {
2348-
match [param1, param2] {
2348+
match [&param1, &param2] {
23492349
[Param::Float(theta), Param::Float(lambda)] => Param::Float(theta + lambda),
2350+
[Param::Float(theta), Param::ParameterExpression(_lambda)] => {
2351+
add_param(&param2, *theta, py)
2352+
}
2353+
[Param::ParameterExpression(_theta), Param::Float(lambda)] => {
2354+
add_param(&param1, *lambda, py)
2355+
}
23502356
[Param::ParameterExpression(theta), Param::ParameterExpression(lambda)] => {
23512357
Param::ParameterExpression(
23522358
theta

qiskit/synthesis/evolution/suzuki_trotter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ def expand(
140140
141141
.. code-block:: text
142142
143-
("X", [0], t), ("ZZ", [0, 1], 2t), ("X", [0], 2)
143+
("X", [0], t), ("ZZ", [0, 1], 2t), ("X", [0], t)
144144
145145
Note that the rotation angle contains a factor of 2, such that that evolution
146146
of a Pauli :math:`P` over time :math:`t`, which is :math:`e^{itP}`, is represented
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
fixes:
3+
- |
4+
The :class:`.PauliEvolutionGate`, if used with a product formula synthesis (this is the default),
5+
did not correctly handle all-identity terms in the operator. The all-identity term
6+
should introduce a global phase equal to ``-evolution_time``, but was off by a factor of 2
7+
and could break for parameterized times. This behavior is now fixed.
8+
Fixed `#13625 <https://github.com/Qiskit/qiskit/issues/13625>`__.

test/python/circuit/library/test_evolution_gate.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,36 @@ def atomic_evolution(pauli, time):
479479
decomposed = evo_gate.definition.decompose()
480480
self.assertEqual(decomposed.count_ops()["cx"], reps * 3 * 4)
481481

482+
def test_all_identity(self):
483+
"""Test circuit with all identity Paulis works correctly."""
484+
evo = PauliEvolutionGate(I ^ I, time=1).definition
485+
expected = QuantumCircuit(2, global_phase=-1)
486+
self.assertEqual(expected, evo)
487+
488+
def test_global_phase(self):
489+
"""Test a circuit with parameterized global phase terms.
490+
491+
Regression test of #13625.
492+
"""
493+
pauli = (X ^ X) + (I ^ I) + (I ^ X)
494+
time = Parameter("t")
495+
evo = PauliEvolutionGate(pauli, time=time)
496+
497+
expected = QuantumCircuit(2, global_phase=-time)
498+
expected.rxx(2 * time, 0, 1)
499+
expected.rx(2 * time, 0)
500+
501+
with self.subTest(msg="check circuit"):
502+
self.assertEqual(expected, evo.definition)
503+
504+
# since all terms in the Pauli operator commute, we can compare to an
505+
# exact matrix exponential
506+
time_value = 1.76123
507+
bound = evo.definition.assign_parameters([time_value])
508+
exact = scipy.linalg.expm(-1j * time_value * pauli.to_matrix())
509+
with self.subTest(msg="check correctness"):
510+
self.assertEqual(Operator(exact), Operator(bound))
511+
482512
def test_sympify_is_real(self):
483513
"""Test converting the parameters to sympy is real.
484514

test/python/circuit/library/test_evolved_op_ansatz.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,21 @@ def test_detect_commutation(self):
199199
# this Hamiltonian should be split into 2 commuting groups, hence we get 2 parameters
200200
self.assertEqual(2, circuit.num_parameters)
201201

202+
def test_evolution_with_identity(self):
203+
"""Test a Hamiltonian containing an identity term.
204+
205+
Regression test of #13644.
206+
"""
207+
hamiltonian = SparsePauliOp(["III", "IZZ", "IXI"])
208+
ansatz = hamiltonian_variational_ansatz(hamiltonian, reps=1)
209+
bound = ansatz.assign_parameters([1, 1]) # we have two non-commuting groups, hence 2 params
210+
211+
expected = QuantumCircuit(3, global_phase=-1)
212+
expected.rzz(2, 0, 1)
213+
expected.rx(2, 1)
214+
215+
self.assertEqual(expected, bound)
216+
202217

203218
def evolve(pauli_string, time):
204219
"""Get the reference evolution circuit for a single Pauli string."""

0 commit comments

Comments
 (0)