Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix inner qubit mapping in UnitarySynthesis (backport #10405) #10431

Merged
merged 1 commit into from
Jul 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 31 additions & 21 deletions qiskit/transpiler/passes/synthesis/unitary_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
)
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
32 changes: 32 additions & 0 deletions test/python/transpiler/test_unitary_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down