Skip to content

Commit

Permalink
Avoid intermediate DAGCircuit construction in 2q synthesis (#12179)
Browse files Browse the repository at this point in the history
This commit builds on #12109 which added a dag output to the two qubit
decomposers that are then used by unitary synthesis to add a mode of
operation in unitary synthesis that avoids intermediate dag creation.
To do this efficiently this requires changing the UnitarySynthesis pass
to rebuild the DAG instead of doing a node substitution.
  • Loading branch information
mtreinish committed Apr 30, 2024
1 parent 9c22197 commit 958cc9b
Showing 1 changed file with 98 additions and 33 deletions.
131 changes: 98 additions & 33 deletions qiskit/transpiler/passes/synthesis/unitary_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from qiskit.synthesis.two_qubit.two_qubit_decompose import (
TwoQubitBasisDecomposer,
TwoQubitWeylDecomposition,
GATE_NAME_MAP,
)
from qiskit.quantum_info import Operator
from qiskit.circuit import ControlFlowOp, Gate, Parameter
Expand Down Expand Up @@ -293,7 +294,7 @@ def __init__(
natural_direction: bool | None = None,
synth_gates: list[str] | None = None,
method: str = "default",
min_qubits: int = None,
min_qubits: int = 0,
plugin_config: dict = None,
target: Target = None,
):
Expand Down Expand Up @@ -499,27 +500,55 @@ def _run_main_loop(
]
)

for node in dag.named_nodes(*self._synth_gates):
if self._min_qubits is not None and len(node.qargs) < self._min_qubits:
continue
synth_dag = None
unitary = node.op.to_matrix()
n_qubits = len(node.qargs)
if (plugin_method.max_qubits is not None and n_qubits > plugin_method.max_qubits) or (
plugin_method.min_qubits is not None and n_qubits < plugin_method.min_qubits
):
method, kwargs = default_method, default_kwargs
out_dag = dag.copy_empty_like()
for node in dag.topological_op_nodes():
if node.op.name == "unitary" and len(node.qargs) >= self._min_qubits:
synth_dag = None
unitary = node.op.to_matrix()
n_qubits = len(node.qargs)
if (
plugin_method.max_qubits is not None and n_qubits > plugin_method.max_qubits
) or (plugin_method.min_qubits is not None and n_qubits < plugin_method.min_qubits):
method, kwargs = default_method, default_kwargs
else:
method, kwargs = plugin_method, plugin_kwargs
if method.supports_coupling_map:
kwargs["coupling_map"] = (
self._coupling_map,
[qubit_indices[x] for x in node.qargs],
)
synth_dag = method.run(unitary, **kwargs)
if synth_dag is None:
out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False)
continue
if isinstance(synth_dag, DAGCircuit):
qubit_map = dict(zip(synth_dag.qubits, node.qargs))
for node in synth_dag.topological_op_nodes():
out_dag.apply_operation_back(
node.op, (qubit_map[x] for x in node.qargs), check=False
)
out_dag.global_phase += synth_dag.global_phase
else:
node_list, global_phase, gate = synth_dag
qubits = node.qargs
for (
op_name,
params,
qargs,
) in node_list:
if op_name == "USER_GATE":
op = gate
else:
op = GATE_NAME_MAP[op_name](*params)
out_dag.apply_operation_back(
op,
(qubits[x] for x in qargs),
check=False,
)
out_dag.global_phase += global_phase
else:
method, kwargs = plugin_method, plugin_kwargs
if method.supports_coupling_map:
kwargs["coupling_map"] = (
self._coupling_map,
[qubit_indices[x] for x in node.qargs],
)
synth_dag = method.run(unitary, **kwargs)
if synth_dag is not None:
dag.substitute_node_with_dag(node, synth_dag)
return dag
out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False)
return out_dag


def _build_gate_lengths(props=None, target=None):
Expand Down Expand Up @@ -893,6 +922,20 @@ def run(self, unitary, **options):
decomposers2q = [decomposer2q] if decomposer2q is not None else []
# choose the cheapest output among synthesized circuits
synth_circuits = []
# If we have a single TwoQubitBasisDecomposer skip dag creation as we don't need to
# store and can instead manually create the synthesized gates directly in the output dag
if len(decomposers2q) == 1 and isinstance(decomposers2q[0], TwoQubitBasisDecomposer):
preferred_direction = _preferred_direction(
decomposers2q[0],
qubits,
natural_direction,
coupling_map,
gate_lengths,
gate_errors,
)
return self._synth_su4_no_dag(
unitary, decomposers2q[0], preferred_direction, approximation_degree
)
for decomposer2q in decomposers2q:
preferred_direction = _preferred_direction(
decomposer2q, qubits, natural_direction, coupling_map, gate_lengths, gate_errors
Expand All @@ -919,6 +962,24 @@ def run(self, unitary, **options):
return synth_circuit
return circuit_to_dag(synth_circuit)

def _synth_su4_no_dag(self, unitary, decomposer2q, preferred_direction, approximation_degree):
approximate = not approximation_degree == 1.0
synth_circ = decomposer2q._inner_decomposer(unitary, approximate=approximate)
if not preferred_direction:
return (synth_circ, synth_circ.global_phase, decomposer2q.gate)

synth_direction = None
# if the gates in synthesis are in the opposite direction of the preferred direction
# resynthesize a new operator which is the original conjugated by swaps.
# this new operator is doubly mirrored from the original and is locally equivalent.
for op_name, _params, qubits in synth_circ:
if op_name in {"USER_GATE", "cx"}:
synth_direction = qubits
if synth_direction is not None and synth_direction != preferred_direction:
# TODO: Avoid using a dag to correct the synthesis direction
return self._reversed_synth_su4(unitary, decomposer2q, approximation_degree)
return (synth_circ, synth_circ.global_phase, decomposer2q.gate)

def _synth_su4(self, su4_mat, decomposer2q, preferred_direction, approximation_degree):
approximate = not approximation_degree == 1.0
synth_circ = decomposer2q(su4_mat, approximate=approximate, use_dag=True)
Expand All @@ -932,16 +993,20 @@ def _synth_su4(self, su4_mat, decomposer2q, preferred_direction, approximation_d
if inst.op.num_qubits == 2:
synth_direction = [synth_circ.find_bit(q).index for q in inst.qargs]
if synth_direction is not None and synth_direction != preferred_direction:
su4_mat_mm = su4_mat.copy()
su4_mat_mm[[1, 2]] = su4_mat_mm[[2, 1]]
su4_mat_mm[:, [1, 2]] = su4_mat_mm[:, [2, 1]]
synth_circ = decomposer2q(su4_mat_mm, approximate=approximate, use_dag=True)
out_dag = DAGCircuit()
out_dag.global_phase = synth_circ.global_phase
out_dag.add_qubits(list(reversed(synth_circ.qubits)))
flip_bits = out_dag.qubits[::-1]
for node in synth_circ.topological_op_nodes():
qubits = tuple(flip_bits[synth_circ.find_bit(x).index] for x in node.qargs)
out_dag.apply_operation_back(node.op, qubits, check=False)
return out_dag
return self._reversed_synth_su4(su4_mat, decomposer2q, approximation_degree)
return synth_circ

def _reversed_synth_su4(self, su4_mat, decomposer2q, approximation_degree):
approximate = not approximation_degree == 1.0
su4_mat_mm = su4_mat.copy()
su4_mat_mm[[1, 2]] = su4_mat_mm[[2, 1]]
su4_mat_mm[:, [1, 2]] = su4_mat_mm[:, [2, 1]]
synth_circ = decomposer2q(su4_mat_mm, approximate=approximate, use_dag=True)
out_dag = DAGCircuit()
out_dag.global_phase = synth_circ.global_phase
out_dag.add_qubits(list(reversed(synth_circ.qubits)))
flip_bits = out_dag.qubits[::-1]
for node in synth_circ.topological_op_nodes():
qubits = tuple(flip_bits[synth_circ.find_bit(x).index] for x in node.qargs)
out_dag.apply_operation_back(node.op, qubits, check=False)
return out_dag

0 comments on commit 958cc9b

Please sign in to comment.