From e471c580d92914f73b5b840a88cdbe84f40e1ec3 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Thu, 6 Jul 2023 17:33:00 -0400 Subject: [PATCH] Fix inner qubit mapping in UnitarySynthesis pass. --- .../passes/synthesis/unitary_synthesis.py | 52 +++++++++++-------- ...synthesis-cf-mapping-fe9bd2e5fbd56dfb.yaml | 6 +++ .../transpiler/test_unitary_synthesis.py | 32 ++++++++++++ 3 files changed, 69 insertions(+), 21 deletions(-) create mode 100644 releasenotes/notes/fix-synthesis-cf-mapping-fe9bd2e5fbd56dfb.yaml diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 82fcb829b0f2..67176ddf374e 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -19,7 +19,7 @@ from functools import partial import numpy as np -from qiskit.converters import circuit_to_dag +from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.transpiler import CouplingMap, Target from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError @@ -41,7 +41,6 @@ ECRGate, ) from qiskit.transpiler.passes.synthesis import plugin -from qiskit.transpiler.passes.utils import control_flow from qiskit.transpiler.passes.optimization.optimize_1q_decomposition import ( Optimize1qGatesDecomposition, ) @@ -442,29 +441,40 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: if self.method == "default": # pylint: disable=attribute-defined-outside-init plugin_method._approximation_degree = self._approximation_degree - return self._run_main_loop( - dag, plugin_method, plugin_kwargs, default_method, default_kwargs - ) - - def _run_main_loop(self, dag, plugin_method, plugin_kwargs, default_method, default_kwargs): - """Inner loop for the optimizer, after all DAG-independent set-up has been completed.""" - - def _recurse(dag): - # This isn't quite a trivially recursive call because we need to close over the - # arguments to the function. The loop is sufficiently long that it's cleaner to do it - # in a separate method rather than define a helper closure within `self.run`. - return self._run_main_loop( - dag, plugin_method, plugin_kwargs, default_method, default_kwargs - ) - - for node in dag.op_nodes(ControlFlowOp): - node.op = control_flow.map_blocks(_recurse, node.op) - dag_bit_indices = ( + qubit_indices = ( {bit: i for i, bit in enumerate(dag.qubits)} if plugin_method.supports_coupling_map or default_method.supports_coupling_map else {} ) + return self._run_main_loop( + dag, qubit_indices, plugin_method, plugin_kwargs, default_method, default_kwargs + ) + + def _run_main_loop( + self, dag, qubit_indices, plugin_method, plugin_kwargs, default_method, default_kwargs + ): + """Inner loop for the optimizer, after all DAG-independent set-up has been completed.""" + for node in dag.op_nodes(ControlFlowOp): + node.op = node.op.replace_blocks( + [ + dag_to_circuit( + self._run_main_loop( + circuit_to_dag(block), + { + inner: qubit_indices[outer] + for inner, outer in zip(block.qubits, node.qargs) + }, + plugin_method, + plugin_kwargs, + default_method, + default_kwargs, + ), + copy_operations=False, + ) + for block in node.op.blocks + ] + ) for node in dag.named_nodes(*self._synth_gates): if self._min_qubits is not None and len(node.qargs) < self._min_qubits: @@ -481,7 +491,7 @@ def _recurse(dag): if method.supports_coupling_map: kwargs["coupling_map"] = ( self._coupling_map, - [dag_bit_indices[x] for x in node.qargs], + [qubit_indices[x] for x in node.qargs], ) synth_dag = method.run(unitary, **kwargs) if synth_dag is not None: diff --git a/releasenotes/notes/fix-synthesis-cf-mapping-fe9bd2e5fbd56dfb.yaml b/releasenotes/notes/fix-synthesis-cf-mapping-fe9bd2e5fbd56dfb.yaml new file mode 100644 index 000000000000..c713facfd8fd --- /dev/null +++ b/releasenotes/notes/fix-synthesis-cf-mapping-fe9bd2e5fbd56dfb.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed an issue with :class:`.UnitarySynthesis` when using the ``target`` + parameter where circuits with control flow were not properly mapped + to the target. diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index 4aaecbf2e1a3..d93b03934173 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -809,6 +809,38 @@ def test_nested_control_flow(self): self.assertEqual(cbody.count_ops().keys(), {"u", "cx"}) self.assertEqual(qc_uni1_mat, Operator(cbody)) + def test_mapping_control_flow(self): + """Test that inner dags use proper qubit mapping.""" + qr = QuantumRegister(3, "q") + qc = QuantumCircuit(qr) + + # Create target that supports CX only between 0 and 2. + fake_target = Target() + fake_target.add_instruction(CXGate(), {(0, 2): None}) + fake_target.add_instruction( + UGate(Parameter("t"), Parameter("p"), Parameter("l")), + { + (0,): None, + (1,): None, + (2,): None, + }, + ) + + qc_uni1 = QuantumCircuit(2) + qc_uni1.swap(0, 1) + qc_uni1_mat = Operator(qc_uni1) + + loop_body = QuantumCircuit(2) + loop_body.unitary(qc_uni1_mat, [0, 1]) + + # Loop body uses qubits 0 and 2, mapped to 0 and 1 in the block. + # If synthesis doesn't handle recursive mapping, it'll incorrectly + # look for a CX on (0, 1) instead of on (0, 2). + qc.for_loop((0,), None, loop_body, [0, 2], []) + + dag = circuit_to_dag(qc) + UnitarySynthesis(basis_gates=["u", "cx"], target=fake_target).run(dag) + def test_single_qubit_with_target(self): """Test input circuit with only 1q works with target.""" qc = QuantumCircuit(1)