From ff133cf51e74c26fe306fcd492abb344ea3f62ee Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 3 Feb 2025 12:56:51 +0000 Subject: [PATCH] Add `ContractIdleWiresInControlFlow` optimisation pass This transpiler pass removes data dependencies on idle qubits from control-flow operations. For example, given a circuit such as:: from qiskit.circuit import QuantumCircuit qc = QuantumCircuit(1, 1) qc.x(0) with qc.if_test((qc.clbits[0], True)): qc.x(0) qc.x(0) qc.x(0) the current optimisation passes will collapse the inner control-flow block to the identity, but the qubit dependency will remain, preventing the outer two X gates from being cancelled. This pass removes the now-spurious dependency, making it possible to detect and remove the two X gates in a follow-up loop iteration. As an accidental side-effect of their algorithms, the control-flow-aware routing passes currently do this when they run. This aims to move the logic into a suitable place to run before routing (so the spurious dependency never arises in routing in the first place) and in the low-level optimisation stage. The aim of this pass is also to centralise the logic, so when the addition of the new `box` scope with different semantics around whether a wire is truly idle in the box or not, the routers aren't accidentally breaking them, and it's clearer when the modifications happen. --- qiskit/transpiler/passes/__init__.py | 2 + .../passes/optimization/__init__.py | 1 + .../contract_idle_wires_in_control_flow.py | 104 ++++++++++ ...ires-in-control-flow-264f7c92396b217e.yaml | 8 + ...est_contract_idle_wires_in_control_flow.py | 183 ++++++++++++++++++ 5 files changed, 298 insertions(+) create mode 100644 qiskit/transpiler/passes/optimization/contract_idle_wires_in_control_flow.py create mode 100644 releasenotes/notes/contract-idle-wires-in-control-flow-264f7c92396b217e.yaml create mode 100644 test/python/transpiler/test_contract_idle_wires_in_control_flow.py diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index 8823e1ce7233..192a50ec9dd8 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -93,6 +93,7 @@ OptimizeAnnotated Split2QUnitaries RemoveIdentityEquivalent + ContractIdleWiresInControlFlow Calibration ============= @@ -248,6 +249,7 @@ from .optimization import OptimizeAnnotated from .optimization import RemoveIdentityEquivalent from .optimization import Split2QUnitaries +from .optimization import ContractIdleWiresInControlFlow # circuit analysis from .analysis import ResourceEstimation diff --git a/qiskit/transpiler/passes/optimization/__init__.py b/qiskit/transpiler/passes/optimization/__init__.py index 0e5108f44d2a..50e3fd9e35fc 100644 --- a/qiskit/transpiler/passes/optimization/__init__.py +++ b/qiskit/transpiler/passes/optimization/__init__.py @@ -40,3 +40,4 @@ from .remove_identity_equiv import RemoveIdentityEquivalent from .split_2q_unitaries import Split2QUnitaries from .collect_and_collapse import CollectAndCollapse +from .contract_idle_wires_in_control_flow import ContractIdleWiresInControlFlow diff --git a/qiskit/transpiler/passes/optimization/contract_idle_wires_in_control_flow.py b/qiskit/transpiler/passes/optimization/contract_idle_wires_in_control_flow.py new file mode 100644 index 000000000000..ae4ecdd48a04 --- /dev/null +++ b/qiskit/transpiler/passes/optimization/contract_idle_wires_in_control_flow.py @@ -0,0 +1,104 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2025 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Contract control-flow operations that contain idle wires.""" + +from qiskit.circuit import Qubit, Clbit, QuantumCircuit +from qiskit.dagcircuit import DAGCircuit, DAGOpNode +from qiskit.transpiler.basepasses import TransformationPass + + +class ContractIdleWiresInControlFlow(TransformationPass): + """Remove idle qubits from control-flow operations of a :class:`.DAGCircuit`.""" + + def run(self, dag): + # `control_flow_op_nodes` is eager and doesn't borrow; we're mutating the DAG in the loop. + for node in dag.control_flow_op_nodes(): + inst = node._to_circuit_instruction() + new_inst = _contract_control_flow(inst) + if new_inst is inst: + # No top-level contraction; nothing to do. + continue + replacement = DAGCircuit() + # Dictionaries to retain insertion order for reproducibility, and because we can + # then re-use them as mapping dictionaries. + qubits, clbits, vars_ = {}, {}, {} + for _, _, wire in dag.edges(node): + if isinstance(wire, Qubit): + qubits[wire] = wire + elif isinstance(wire, Clbit): + clbits[wire] = wire + else: + vars_[wire] = wire + replacement.add_qubits(list(qubits)) + replacement.add_clbits(list(clbits)) + for var in vars_: + replacement.add_captured_var(var) + replacement._apply_op_node_back(DAGOpNode.from_instruction(new_inst)) + # The replacement DAG is defined over all the same qubits, but with the correct + # qubits now explicitly marked as idle, so everything gets linked up correctly. + dag.substitute_node_with_dag( + node, replacement, wires=qubits | clbits | vars_, propagate_condition=False + ) + return dag + + +def _contract_control_flow(inst): + """Contract a `CircuitInstruction` containing a control-flow operation. + + Returns the input object by the same reference if there's no contraction to be done at the call + site, though nested control-flow ops may have been contracted in place.""" + op = inst.operation + idle = set(inst.qubits) + for block in op.blocks: + qubit_map = dict(zip(block.qubits, inst.qubits)) + for i, inner in enumerate(block.data): + if inner.is_control_flow(): + # In `QuantumCircuit` it's easy to replace an instruction with a narrower one, so it + # doesn't matter much if this is replacing it with itself. + block.data[i] = inner = _contract_control_flow(inner) + for qubit in inner.qubits: + idle.discard(qubit_map[qubit]) + # If a box, we still want the prior side-effect of contracting any internal control-flow + # operations (optimisations are still valid _within_ a box), but we don't want to contract the + # box itself. If there's no idle qubits, we're also done here. + if not idle or inst.name == "box": + return inst + + def contract(block): + out = QuantumCircuit( + name=block.name, + global_phase=block.global_phase, + metadata=block.metadata, + captures=block.iter_captured_vars(), + ) + out.add_bits( + [ + block_qubit + for (block_qubit, inst_qubit) in zip(block.qubits, inst.qubits) + if inst_qubit not in idle + ] + ) + out.add_bits(block.clbits) + for creg in block.cregs: + out.add_register(creg) + # Control-flow ops can only have captures and locals, and we already added the captures. + for var in block.iter_declared_vars(): + out.add_uninitialized_var(var) + for inner in block: + out._append(inner) + return out + + return inst.replace( + operation=op.replace_blocks(contract(block) for block in op.blocks), + qubits=[qubit for qubit in inst.qubits if qubit not in idle], + ) diff --git a/releasenotes/notes/contract-idle-wires-in-control-flow-264f7c92396b217e.yaml b/releasenotes/notes/contract-idle-wires-in-control-flow-264f7c92396b217e.yaml new file mode 100644 index 000000000000..679ddb103645 --- /dev/null +++ b/releasenotes/notes/contract-idle-wires-in-control-flow-264f7c92396b217e.yaml @@ -0,0 +1,8 @@ +--- +features_transpiler: + - | + A new transpiler pass, :class:`.ContractIdleWiresInControlFlow`, is available from + :mod:`qiskit.transpiler.passes`. This pass removes qubits from control-flow blocks if the + semantics allow this, and the qubit is idle throughout the control-flow operation. Previously, + the routing stage of the preset pass managers might have done this as an accidental side-effect + of how they worked, but the behavior is now more properly placed in an optimization pass. diff --git a/test/python/transpiler/test_contract_idle_wires_in_control_flow.py b/test/python/transpiler/test_contract_idle_wires_in_control_flow.py new file mode 100644 index 000000000000..f2b3534532f7 --- /dev/null +++ b/test/python/transpiler/test_contract_idle_wires_in_control_flow.py @@ -0,0 +1,183 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2025 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring + +from qiskit.circuit import QuantumCircuit, ClassicalRegister, QuantumRegister +from qiskit.circuit.classical import expr, types +from qiskit.transpiler.passes import ContractIdleWiresInControlFlow + +from test import QiskitTestCase # pylint: disable=wrong-import-order + + +class TestContractIdleWiresInControlFlow(QiskitTestCase): + def test_simple_body(self): + qc = QuantumCircuit(3, 1) + with qc.while_loop((qc.clbits[0], False)): + qc.cx(0, 1) + qc.noop(2) + + expected = QuantumCircuit(3, 1) + with expected.while_loop((expected.clbits[0], False)): + expected.cx(0, 1) + + self.assertEqual(ContractIdleWiresInControlFlow()(qc), expected) + + def test_nothing_to_do(self): + qc = QuantumCircuit(3, 1) + with qc.for_loop(range(3)): + qc.h(0) + qc.cx(0, 1) + self.assertEqual(ContractIdleWiresInControlFlow()(qc), qc) + + def test_disparate_if_else_left_alone(self): + qc = QuantumCircuit(3, 1) + # The true body only uses 0, the false body only uses (1, 2), but because they're part of + # the shared op, there is no valid contraction here. + with qc.if_test((qc.clbits[0], True)) as else_: + qc.h(0) + with else_: + qc.cx(1, 2) + self.assertEqual(ContractIdleWiresInControlFlow()(qc), qc) + + def test_contract_if_else_both_bodies(self): + qc = QuantumCircuit(3, 1) + # Explicit idle in the true body only. + with qc.if_test((qc.clbits[0], True)) as else_: + qc.h(0) + qc.cx(0, 2) + qc.noop(1) + with else_: + qc.cz(0, 2) + # Explicit idle in the false body only. + with qc.if_test((qc.clbits[0], True)) as else_: + qc.h(0) + qc.cx(0, 1) + with else_: + qc.cz(0, 1) + qc.noop(2) + # Explicit idle in both bodies. + with qc.if_test((qc.clbits[0], True)) as else_: + qc.h(1) + qc.cx(1, 2) + qc.noop(0) + with else_: + qc.cz(1, 2) + qc.noop(0) + + expected = QuantumCircuit(3, 1) + with expected.if_test((expected.clbits[0], True)) as else_: + expected.h(0) + expected.cx(0, 2) + with else_: + expected.cz(0, 2) + with expected.if_test((expected.clbits[0], True)) as else_: + expected.h(0) + expected.cx(0, 1) + with else_: + expected.cz(0, 1) + with expected.if_test((expected.clbits[0], True)) as else_: + expected.h(1) + expected.cx(1, 2) + with else_: + expected.cz(1, 2) + + self.assertEqual(ContractIdleWiresInControlFlow()(qc), expected) + + def test_recursively_contract(self): + qc = QuantumCircuit(3, 1) + with qc.if_test((qc.clbits[0], True)): + qc.h(0) + with qc.if_test((qc.clbits[0], True)): + qc.cx(0, 1) + qc.noop(2) + with qc.while_loop((qc.clbits[0], True)): + with qc.if_test((qc.clbits[0], True)) as else_: + qc.h(0) + qc.noop(1, 2) + with else_: + qc.cx(0, 1) + qc.noop(2) + + expected = QuantumCircuit(3, 1) + with expected.if_test((expected.clbits[0], True)): + expected.h(0) + with expected.if_test((expected.clbits[0], True)): + expected.cx(0, 1) + with expected.while_loop((expected.clbits[0], True)): + with expected.if_test((expected.clbits[0], True)) as else_: + expected.h(0) + with else_: + expected.cx(0, 1) + + actual = ContractIdleWiresInControlFlow()(qc) + self.assertNotEqual(qc, actual) # Smoke test. + self.assertEqual(actual, expected) + + def test_handles_vars_in_contraction(self): + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + c = expr.Var.new("c", types.Bool()) + + qc = QuantumCircuit(3, inputs=[a]) + qc.add_var(b, 5) + with qc.if_test(a): + qc.add_var(c, False) + with qc.if_test(c): + qc.x(0) + qc.noop(1, 2) + with qc.switch(b) as case: + with case(0): + qc.x(0) + with case(1): + qc.noop(0, 1) + with case(case.DEFAULT): + with qc.if_test(a): + qc.x(0) + qc.noop(1, 2) + + expected = QuantumCircuit(3, inputs=[a]) + expected.add_var(b, 5) + with expected.if_test(a): + expected.add_var(c, False) + with expected.if_test(c): + expected.x(0) + with expected.switch(b) as case: + with case(0): + expected.x(0) + with case(1): + pass + with case(case.DEFAULT): + with expected.if_test(a): + expected.x(0) + + actual = ContractIdleWiresInControlFlow()(qc) + self.assertNotEqual(qc, actual) # Smoke test. + self.assertEqual(actual, expected) + + def test_handles_registers_in_contraction(self): + qr = QuantumRegister(3, "q") + cr1 = ClassicalRegister(3, "cr1") + cr2 = ClassicalRegister(3, "cr2") + + qc = QuantumCircuit(qr, cr1, cr2) + with qc.if_test((cr1, 3)): + with qc.if_test((cr2, 3)): + qc.noop(0, 1, 2) + expected = QuantumCircuit(qr, cr1, cr2) + with expected.if_test((cr1, 3)): + with expected.if_test((cr2, 3)): + pass + + actual = ContractIdleWiresInControlFlow()(qc) + self.assertNotEqual(qc, actual) # Smoke test. + self.assertEqual(actual, expected)