Skip to content

Commit fa215e4

Browse files
authored
Add control flow support to ConvertToPauliRotations (#15941)
* add trivial_recurse to CTPR run method * allow control flow instructions in the DAG for this pass * release notes * test simple and nested control flow * cargo fmt changes * lint fixes * fix control flow and release notes * rewrite name-based barrier/reset/delay check * rust file modified * add parameter to nested control flow test * release note typo correction * fix release note typos * minor fixes to address Julien's comments * lint fix * fix parameterized gates test * lint fix * remove global phase line
1 parent 5bf8919 commit fa215e4

4 files changed

Lines changed: 73 additions & 5 deletions

File tree

crates/transpiler/src/passes/convert_to_pauli_rotations.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType};
2020
use qiskit_circuit::instruction::Parameters;
2121
use qiskit_circuit::operations::{
2222
Operation, OperationRef, Param, PauliProductMeasurement, PauliProductRotation, StandardGate,
23-
add_param, multiply_param, radd_param,
23+
StandardInstruction, add_param, multiply_param, radd_param,
2424
};
2525
use qiskit_circuit::{BlocksMode, Qubit, VarsMode};
2626
use qiskit_quantum_info::sparse_observable::BitTerm;
@@ -469,7 +469,7 @@ fn generate_pauli_product_rotation_gate(paulis: &[BitTerm], angle: Param) -> Pau
469469
#[pyfunction]
470470
#[pyo3(name = "convert_to_pauli_rotations")]
471471
pub fn py_convert_to_pauli_rotations(dag: &DAGCircuit) -> PyResult<DAGCircuit> {
472-
let mut new_dag = dag.copy_empty_like(VarsMode::Alike, BlocksMode::Drop)?;
472+
let mut new_dag = dag.copy_empty_like(VarsMode::Alike, BlocksMode::Keep)?;
473473

474474
// Iterate over nodes in the DAG and collect nodes
475475
let mut global_phase = Param::Float(0.0);
@@ -478,7 +478,13 @@ pub fn py_convert_to_pauli_rotations(dag: &DAGCircuit) -> PyResult<DAGCircuit> {
478478
let NodeType::Operation(inst) = &dag[node_index] else {
479479
unreachable!("dag.topological_op_nodes only returns Operations");
480480
};
481-
if ["barrier", "reset", "delay"].contains(&inst.op.name()) {
481+
if matches!(
482+
inst.op.view(),
483+
OperationRef::ControlFlow(_)
484+
| OperationRef::StandardInstruction(StandardInstruction::Barrier(_))
485+
| OperationRef::StandardInstruction(StandardInstruction::Reset)
486+
| OperationRef::StandardInstruction(StandardInstruction::Delay(_))
487+
) {
482488
new_dag.push_back(inst.clone())?;
483489
} else if inst.op.name() == "measure" {
484490
let z = vec![true];

qiskit/transpiler/passes/optimization/convert_to_pauli_rotations.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from qiskit.transpiler.basepasses import TransformationPass
1616
from qiskit.dagcircuit import DAGCircuit
17+
from qiskit.transpiler.passes.utils import control_flow
1718
from qiskit._accelerate.convert_to_pauli_rotations import convert_to_pauli_rotations
1819

1920

@@ -46,6 +47,7 @@ class ConvertToPauliRotations(TransformationPass):
4647
assert Operator(qc) == Operator(qct)
4748
"""
4849

50+
@control_flow.trivial_recurse
4951
def run(self, dag: DAGCircuit) -> DAGCircuit:
5052
"""Run the ConvertToPauliRotations optimization pass on ``dag``.
5153
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
features_transpiler:
3+
- The pass :class:`.ConvertToPauliRotations` now supports circuits with
4+
:class:`.ControlFlowOp` by recursively applying the pass on each
5+
constituent control flow block. Previously, it did not support transpilation
6+
of circuits with control flow instructions.

test/python/transpiler/test_convert_to_pauli_rotations.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,21 @@
1313
"""Test ConvertToPauliRotations pass"""
1414

1515
from ddt import ddt
16+
import numpy as np
1617

17-
from qiskit.circuit import QuantumCircuit, Gate, Parameter
18+
from qiskit.circuit import QuantumCircuit, Gate, Parameter, QuantumRegister, ClassicalRegister
1819
from qiskit.transpiler import TranspilerError
1920
from qiskit.transpiler.passes import ConvertToPauliRotations
20-
from qiskit.quantum_info import Operator
21+
from qiskit.quantum_info import Operator, Pauli
2122
from qiskit.circuit.random import random_circuit
2223
from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping
2324
from qiskit.circuit.library import (
2425
C3SXGate,
2526
RC3XGate,
2627
C4XGate,
2728
MCXGate,
29+
PauliProductMeasurement,
30+
PauliProductRotationGate,
2831
)
2932
from test import combine, QiskitTestCase
3033

@@ -130,3 +133,54 @@ def test_unsupported_gates_raise_error(self, gate):
130133

131134
with self.assertRaises(TranspilerError):
132135
_ = ConvertToPauliRotations()(qc)
136+
137+
def test_control_flow(self):
138+
"""Test that simple control flow circuit works with the pass"""
139+
qc = QuantumCircuit(2, 1)
140+
qc.h(0)
141+
qc.measure(0, 0)
142+
qc_true = QuantumCircuit(2, 1)
143+
qc_true.h(0)
144+
qc.if_else((0, True), qc_true, None, range(2), [0])
145+
qct = ConvertToPauliRotations()(qc)
146+
qc_exp = QuantumCircuit(2, 1)
147+
qc_exp.append(PauliProductRotationGate(Pauli("Y"), np.pi / 2), [0])
148+
qc_exp.append(PauliProductRotationGate(Pauli("X"), np.pi), [0])
149+
qc_exp.append(PauliProductMeasurement(Pauli("Z")), [0], [0])
150+
qc_exp.global_phase = np.pi / 2
151+
qc_exp_true = QuantumCircuit(2, 1)
152+
qc_exp_true.global_phase = np.pi / 2
153+
qc_exp_true.append(PauliProductRotationGate(Pauli("Y"), np.pi / 2), [0])
154+
qc_exp_true.append(PauliProductRotationGate(Pauli("X"), np.pi), [0])
155+
qc_exp.if_else((0, True), qc_exp_true, None, range(2), [0])
156+
self.assertEqual(qct, qc_exp)
157+
158+
def test_nested_control_flow(self):
159+
"""Test that nested control flow circuit works with the pass"""
160+
# subcircuit with parameterized gate
161+
theta = Parameter("theta")
162+
qc1 = QuantumCircuit(2)
163+
qc1.h(0)
164+
qc1.ry(theta, 0)
165+
# build input circuit by composing a subcircuit into nested control flow
166+
qr = QuantumRegister(2, "q")
167+
cr = ClassicalRegister(1, "c")
168+
qc = QuantumCircuit(qr, cr)
169+
qc.compose(qc1, [0, 1], inplace=True)
170+
with qc.for_loop(range(3)):
171+
with qc.while_loop((cr, 0)):
172+
qc.compose(qc1, [0, 1], inplace=True)
173+
qct = ConvertToPauliRotations()(qc)
174+
# qc2 is the transpiled equivalent of subcircuit qc1
175+
qc2 = QuantumCircuit(2)
176+
qc2.append(PauliProductRotationGate(Pauli("Y"), np.pi / 2), [0])
177+
qc2.append(PauliProductRotationGate(Pauli("X"), np.pi), [0])
178+
qc2.append(PauliProductRotationGate(Pauli("Y"), theta), [0])
179+
qc2.global_phase = np.pi / 2
180+
# expected output circuit of transpiled qc, should be equivalent to qct
181+
qc_exp = QuantumCircuit(qr, cr)
182+
qc_exp.compose(qc2, [0, 1], inplace=True)
183+
with qc_exp.for_loop(range(3)):
184+
with qc_exp.while_loop((cr, 0)):
185+
qc_exp.compose(qc2, [0, 1], inplace=True)
186+
self.assertEqual(qct, qc_exp)

0 commit comments

Comments
 (0)