From 576efcf14a4be392cbcdb3c75d2838a440f73b13 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 4 Sep 2024 18:32:52 +0300 Subject: [PATCH] Bug fix in `HoareOptimizer` (#13083) * bug fix, test, reno * suggestions from code review; lint --- .../passes/optimization/hoare_opt.py | 20 ++++++----- .../notes/fix-hoare-opt-56d1ca6a07f07a2d.yaml | 5 +++ test/python/transpiler/test_hoare_opt.py | 35 +++++++++++++++++++ 3 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/fix-hoare-opt-56d1ca6a07f07a2d.yaml diff --git a/qiskit/transpiler/passes/optimization/hoare_opt.py b/qiskit/transpiler/passes/optimization/hoare_opt.py index a77f1985f6..e47074fc61 100644 --- a/qiskit/transpiler/passes/optimization/hoare_opt.py +++ b/qiskit/transpiler/passes/optimization/hoare_opt.py @@ -203,20 +203,24 @@ class HoareOptimizer(TransformationPass): """ import z3 - for node in dag.topological_op_nodes(): + # Pre-generate all DAG nodes, since we later iterate over them, while + # potentially modifying and removing some of them. + nodes = list(dag.topological_op_nodes()) + for node in nodes: gate = node.op - ctrlqb, ctrlvar, trgtqb, trgtvar = self._seperate_ctrl_trgt(node) + _, ctrlvar, trgtqb, trgtvar = self._seperate_ctrl_trgt(node) ctrl_ones = z3.And(*ctrlvar) - remove_ctrl, new_dag, qb_idx = self._remove_control(gate, ctrlvar, trgtvar) + remove_ctrl, new_dag, _ = self._remove_control(gate, ctrlvar, trgtvar) if remove_ctrl: - dag.substitute_node_with_dag(node, new_dag) - gate = gate.base_gate - node.op = gate.to_mutable() - node.name = gate.name - node.qargs = tuple((ctrlqb + trgtqb)[qi] for qi in qb_idx) + # We are replacing a node by a new node over a smaller number of qubits. + # This can be done using substitute_node_with_dag, which furthermore returns + # a mapping from old node ids to new nodes. + mapped_nodes = dag.substitute_node_with_dag(node, new_dag) + node = next(iter(mapped_nodes.values())) + gate = node.op _, ctrlvar, trgtqb, trgtvar = self._seperate_ctrl_trgt(node) ctrl_ones = z3.And(*ctrlvar) diff --git a/releasenotes/notes/fix-hoare-opt-56d1ca6a07f07a2d.yaml b/releasenotes/notes/fix-hoare-opt-56d1ca6a07f07a2d.yaml new file mode 100644 index 0000000000..7548dcc995 --- /dev/null +++ b/releasenotes/notes/fix-hoare-opt-56d1ca6a07f07a2d.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed a bug in :class:`.HoareOptimizer` where a controlled gate was simplified + by removing its controls but the new gate was not handled correctly. diff --git a/test/python/transpiler/test_hoare_opt.py b/test/python/transpiler/test_hoare_opt.py index b3ae3df976..ae2f9d9ae1 100644 --- a/test/python/transpiler/test_hoare_opt.py +++ b/test/python/transpiler/test_hoare_opt.py @@ -661,6 +661,41 @@ class TestHoareOptimizer(QiskitTestCase): self.assertEqual(result2, circuit_to_dag(expected)) + def test_remove_control_then_identity(self): + """This first simplifies a gate by removing its control, then removes the + simplified gate by canceling it with another gate. + See: https://github.com/Qiskit/qiskit-terra/issues/13079 + """ + # ┌───┐┌───┐┌───┐ + # q_0: ┤ X ├┤ X ├┤ X ├ + # └─┬─┘└───┘└─┬─┘ + # q_1: ──■─────────┼── + # ┌───┐ │ + # q_2: ┤ X ├───────■── + # └───┘ + circuit = QuantumCircuit(3) + circuit.cx(1, 0) + circuit.x(2) + circuit.x(0) + circuit.cx(2, 0) + + simplified = HoareOptimizer()(circuit) + + # The CX(1, 0) gate is removed as the control qubit q_1 is initially 0. + # The CX(2, 0) gate is first replaced by X(0) gate as the control qubit q_2 is at 1, + # then the two X(0) gates are removed. + # + # q_0: ───── + # + # q_1: ───── + # ┌───┐ + # q_2: ┤ X ├ + # └───┘ + expected = QuantumCircuit(3) + expected.x(2) + + self.assertEqual(simplified, expected) + if __name__ == "__main__": unittest.main()