From 59d15325112cc34fe4ec7e58bcd4362d3ec8a09b Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sun, 14 Apr 2024 11:24:12 -0400 Subject: [PATCH] Avoid intermediate DAGCircuit construction in 2q synthesis 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. --- .../passes/synthesis/unitary_synthesis.py | 131 +++++++++++++----- 1 file changed, 98 insertions(+), 33 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 5d919661a838..a30411d16a93 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -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 @@ -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, ): @@ -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): @@ -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 @@ -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) @@ -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