Skip to content

Commit

Permalink
Add support for returning a DAGCircuit to TwoQubitBasisDecomposer (#1…
Browse files Browse the repository at this point in the history
…2109)

* Add support for returning a DAGCircuit to TwoQubitBasisDecomposer

This commit adds a new flag, use_dag, to the constructor for the
TwoQubitBasisDecomposer. When set to True, the __call__ method will
return a DAGCircuit instead of a QuantumCircuit. This is useful when the
two qubit basis decomposer is called from within a transpiler context,
as with the UnitarySynthesis pass, to avoid an extra conversion step.

* Pivot to argument on __call__ and add to XXDecomposer too

This commit moves the use_dag flag to the __call__ method directly
instead of storing it as an instance variable. To make the interface
consistent between the 2 built-in decomposers the flag is also added to
the XXDecomposer class's __call__ method too. This was needed because
the unitary synthesis pass calls the decomposers interchangeably and to
be able to use them without type checking they both will need the flag.
  • Loading branch information
mtreinish committed Apr 25, 2024
1 parent 03b9e0b commit f15d758
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 43 deletions.
92 changes: 71 additions & 21 deletions qiskit/synthesis/two_qubit/two_qubit_decompose.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,27 @@
import io
import base64
import warnings
from typing import Optional, Type
from typing import Optional, Type, TYPE_CHECKING

import logging

import numpy as np

from qiskit.circuit import QuantumRegister, QuantumCircuit, Gate
from qiskit.circuit.library.standard_gates import CXGate, U3Gate, U2Gate, U1Gate
from qiskit.circuit.library.standard_gates import (
CXGate,
U3Gate,
U2Gate,
U1Gate,
UGate,
PhaseGate,
RXGate,
RYGate,
RZGate,
SXGate,
XGate,
RGate,
)
from qiskit.exceptions import QiskitError
from qiskit.quantum_info.operators import Operator
from qiskit.synthesis.one_qubit.one_qubit_decompose import (
Expand All @@ -46,9 +59,28 @@
from qiskit.utils.deprecation import deprecate_func
from qiskit._accelerate import two_qubit_decompose

if TYPE_CHECKING:
from qiskit.dagcircuit.dagcircuit import DAGCircuit

logger = logging.getLogger(__name__)


GATE_NAME_MAP = {
"cx": CXGate,
"rx": RXGate,
"sx": SXGate,
"x": XGate,
"rz": RZGate,
"u": UGate,
"p": PhaseGate,
"u1": U1Gate,
"u2": U2Gate,
"u3": U3Gate,
"ry": RYGate,
"r": RGate,
}


def decompose_two_qubit_product_gate(special_unitary_matrix: np.ndarray):
r"""Decompose :math:`U = U_l \otimes U_r` where :math:`U \in SU(4)`,
and :math:`U_l,~U_r \in SU(2)`.
Expand Down Expand Up @@ -481,6 +513,7 @@ class TwoQubitBasisDecomposer:
If ``False``, don't attempt optimization. If ``None``, attempt optimization but don't raise
if unknown.
.. automethod:: __call__
"""

Expand Down Expand Up @@ -585,9 +618,10 @@ def __call__(
unitary: Operator | np.ndarray,
basis_fidelity: float | None = None,
approximate: bool = True,
use_dag: bool = False,
*,
_num_basis_uses: int | None = None,
) -> QuantumCircuit:
) -> QuantumCircuit | DAGCircuit:
r"""Decompose a two-qubit ``unitary`` over fixed basis and :math:`SU(2)` using the best
approximation given that each basis application has a finite ``basis_fidelity``.
Expand All @@ -596,6 +630,8 @@ def __call__(
basis_fidelity (float or None): Fidelity to be assumed for applications of KAK Gate.
If given, overrides ``basis_fidelity`` given at init.
approximate (bool): Approximates if basis fidelities are less than 1.0.
use_dag (bool): If true a :class:`.DAGCircuit` is returned instead of a
:class:`QuantumCircuit` when this class is called.
_num_basis_uses (int): force a particular approximation by passing a number in [0, 3].
Returns:
Expand All @@ -612,26 +648,40 @@ def __call__(
_num_basis_uses=_num_basis_uses,
)
q = QuantumRegister(2)
circ = QuantumCircuit(q, global_phase=sequence.global_phase)
for name, params, qubits in sequence:
try:
getattr(circ, name)(*params, *qubits)
except AttributeError as exc:
if use_dag:
from qiskit.dagcircuit.dagcircuit import DAGCircuit

dag = DAGCircuit()
dag.global_phase = sequence.global_phase
dag.add_qreg(q)
for name, params, qubits in sequence:
if name == "USER_GATE":
circ.append(self.gate, qubits)
elif name == "u3":
gate = U3Gate(*params)
circ.append(gate, qubits)
elif name == "u2":
gate = U2Gate(*params)
circ.append(gate, qubits)
elif name == "u1":
gate = U1Gate(*params)
circ.append(gate, qubits)
dag.apply_operation_back(self.gate, tuple(q[x] for x in qubits), check=False)
else:
raise QiskitError(f"Unknown gate {name}") from exc

return circ
gate = GATE_NAME_MAP[name](*params)
dag.apply_operation_back(gate, tuple(q[x] for x in qubits), check=False)
return dag
else:
circ = QuantumCircuit(q, global_phase=sequence.global_phase)
for name, params, qubits in sequence:
try:
getattr(circ, name)(*params, *qubits)
except AttributeError as exc:
if name == "USER_GATE":
circ.append(self.gate, qubits)
elif name == "u3":
gate = U3Gate(*params)
circ.append(gate, qubits)
elif name == "u2":
gate = U2Gate(*params)
circ.append(gate, qubits)
elif name == "u1":
gate = U1Gate(*params)
circ.append(gate, qubits)
else:
raise QiskitError(f"Unknown gate {name}") from exc

return circ

def traces(self, target):
r"""
Expand Down
8 changes: 7 additions & 1 deletion qiskit/synthesis/two_qubit/xx_decompose/decomposer.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ def __call__(
unitary: Operator | np.ndarray,
basis_fidelity: dict | float | None = None,
approximate: bool = True,
use_dag: bool = False,
) -> QuantumCircuit:
r"""
Fashions a circuit which (perhaps approximately) models the special unitary operation
Expand All @@ -246,6 +247,8 @@ def __call__(
interpreted as ``{pi: f, pi/2: f/2, pi/3: f/3}``.
If given, overrides the basis_fidelity given at init.
approximate (bool): Approximates if basis fidelities are less than 1.0 .
use_dag (bool): If true a :class:`.DAGCircuit` is returned instead of a
:class:`QuantumCircuit` when this class is called.
Returns:
QuantumCircuit: Synthesized circuit.
Expand Down Expand Up @@ -279,7 +282,7 @@ def __call__(
and self.backup_optimizer is not None
):
pi2_fidelity = 1 - strength_to_infidelity[np.pi / 2]
return self.backup_optimizer(unitary, basis_fidelity=pi2_fidelity)
return self.backup_optimizer(unitary, basis_fidelity=pi2_fidelity, use_dag=use_dag)

# change to positive canonical coordinates
if weyl_decomposition.c >= -EPSILON:
Expand Down Expand Up @@ -314,5 +317,8 @@ def __call__(
circ.append(UnitaryGate(weyl_decomposition.K1l), [1])

circ = self._decomposer1q(circ)
if use_dag:
from qiskit.converters import circuit_to_dag

return circuit_to_dag(circ, copy_operations=False)
return circ
70 changes: 49 additions & 21 deletions qiskit/transpiler/passes/synthesis/unitary_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
from __future__ import annotations
from math import pi, inf, isclose
from typing import Any
from copy import deepcopy
from itertools import product
from functools import partial
import numpy as np
Expand Down Expand Up @@ -147,20 +146,26 @@ def _error(circuit, target=None, qubits=None):
of circuit as a weak proxy for error.
"""
if target is None:
return len(circuit)
if isinstance(circuit, DAGCircuit):
return len(circuit.op_nodes())
else:
return len(circuit)
gate_fidelities = []
gate_durations = []
for inst in circuit:
inst_qubits = tuple(qubits[circuit.find_bit(q).index] for q in inst.qubits)

def score_instruction(inst, inst_qubits):
try:
keys = target.operation_names_for_qargs(inst_qubits)
for key in keys:
target_op = target.operation_from_name(key)
if isinstance(target_op, inst.operation.base_class) and (
if isinstance(circuit, DAGCircuit):
op = inst.op
else:
op = inst.operation
if isinstance(target_op, op.base_class) and (
target_op.is_parameterized()
or all(
isclose(float(p1), float(p2))
for p1, p2 in zip(target_op.params, inst.operation.params)
isclose(float(p1), float(p2)) for p1, p2 in zip(target_op.params, op.params)
)
):
inst_props = target[key].get(inst_qubits, None)
Expand All @@ -177,10 +182,22 @@ def _error(circuit, target=None, qubits=None):
else:
raise KeyError
except KeyError as error:
if isinstance(circuit, DAGCircuit):
op = inst.op
else:
op = inst.operation
raise TranspilerError(
f"Encountered a bad synthesis. "
f"Target has no {inst.operation} on qubits {qubits}."
f"Encountered a bad synthesis. " f"Target has no {op} on qubits {qubits}."
) from error

if isinstance(circuit, DAGCircuit):
for inst in circuit.topological_op_nodes():
inst_qubits = tuple(qubits[circuit.find_bit(q).index] for q in inst.qargs)
score_instruction(inst, inst_qubits)
else:
for inst in circuit:
inst_qubits = tuple(qubits[circuit.find_bit(q).index] for q in inst.qubits)
score_instruction(inst, inst_qubits)
# TODO:return np.sum(gate_durations)
return 1 - np.prod(gate_fidelities)

Expand Down Expand Up @@ -896,24 +913,35 @@ def run(self, unitary, **options):

# only decompose if needed. TODO: handle basis better
synth_circuit = qs_decomposition(unitary) if (basis_gates or target) else None

synth_dag = circuit_to_dag(synth_circuit) if synth_circuit is not None else None
return synth_dag
if synth_circuit is None:
return None
if isinstance(synth_circuit, DAGCircuit):
return synth_circuit
return circuit_to_dag(synth_circuit)

def _synth_su4(self, su4_mat, decomposer2q, preferred_direction, approximation_degree):
approximate = not approximation_degree == 1.0
synth_circ = decomposer2q(su4_mat, approximate=approximate)

synth_circ = decomposer2q(su4_mat, approximate=approximate, use_dag=True)
if not preferred_direction:
return synth_circ
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.
synth_direction = None
for inst in synth_circ:
if inst.operation.num_qubits == 2:
synth_direction = [synth_circ.find_bit(q).index for q in inst.qubits]
if preferred_direction and synth_direction != preferred_direction:
su4_mat_mm = deepcopy(su4_mat)
for inst in synth_circ.topological_op_nodes():
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).reverse_bits()
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 synth_circ
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
features_synthesis:
- |
Added a new argument, ``use_dag``, to the :meth:`.TwoQubitBasisDecomposer.__call__`
and :meth:`.XXDecomposer.__call__` methods. This argument is used to control whether
a :class:`.DAGCircuit` is returned when calling a :class:`.TwoQubitBasisDecomposer`
or :class:`.XXDecomposer` instance instead of the default :class:`.QuantumCircuit`.
For example::
from qiskit.circuit.library import CXGate
from qiskit.quantum_info import random_unitary
from qiskit.synthesis import TwoQubitBasisDecomposer
decomposer = TwoQubitBasisDecomposer(CXGate(), euler_basis="PSX")
decomposer(random_unitary(4), use_dag=True)
will return a :class:`.DAGCircuit` when calling the :class:`.TwoQubitBasisDecomposer`
instance ``decomposer``.
39 changes: 39 additions & 0 deletions test/python/synthesis/test_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from ddt import ddt, data

from qiskit import QiskitError, transpile
from qiskit.dagcircuit.dagcircuit import DAGCircuit
from qiskit.circuit import QuantumCircuit, QuantumRegister
from qiskit.converters import dag_to_circuit, circuit_to_dag
from qiskit.circuit.library import (
Expand Down Expand Up @@ -270,6 +271,8 @@ def check_exact_decomposition(
):
"""Check exact decomposition for a particular target"""
decomp_circuit = decomposer(target_unitary, _num_basis_uses=num_basis_uses)
if isinstance(decomp_circuit, DAGCircuit):
decomp_circuit = dag_to_circuit(decomp_circuit)
if num_basis_uses is not None:
self.assertEqual(num_basis_uses, decomp_circuit.count_ops().get("unitary", 0))
decomp_unitary = Operator(decomp_circuit).data
Expand Down Expand Up @@ -1232,6 +1235,42 @@ def test_euler_basis_selection(self, euler_bases, kak_gates, seed):
requested_basis = set(oneq_gates + [kak_gate_name])
self.assertTrue(decomposition_basis.issubset(requested_basis))

@combine(
seed=range(10),
euler_bases=[
("U321", ["u3", "u2", "u1"]),
("U3", ["u3"]),
("U", ["u"]),
("U1X", ["u1", "rx"]),
("RR", ["r"]),
("PSX", ["p", "sx"]),
("ZYZ", ["rz", "ry"]),
("ZXZ", ["rz", "rx"]),
("XYX", ["rx", "ry"]),
("ZSX", ["rz", "sx"]),
("ZSXX", ["rz", "sx", "x"]),
],
kak_gates=[
(CXGate(), "cx"),
(CZGate(), "cz"),
(iSwapGate(), "iswap"),
(RXXGate(np.pi / 2), "rxx"),
],
name="test_euler_basis_selection_{seed}_{euler_bases[0]}_{kak_gates[1]}",
)
def test_use_dag(self, euler_bases, kak_gates, seed):
"""Test the use_dag flag returns a correct dagcircuit with various target bases."""
(euler_basis, oneq_gates) = euler_bases
(kak_gate, kak_gate_name) = kak_gates
with self.subTest(euler_basis=euler_basis, kak_gate=kak_gate):
decomposer = TwoQubitBasisDecomposer(kak_gate, euler_basis=euler_basis)
unitary = random_unitary(4, seed=seed)
self.assertIsInstance(decomposer(unitary, use_dag=True), DAGCircuit)
self.check_exact_decomposition(unitary.data, decomposer)
decomposition_basis = set(decomposer(unitary).count_ops())
requested_basis = set(oneq_gates + [kak_gate_name])
self.assertTrue(decomposition_basis.issubset(requested_basis))


@ddt
class TestPulseOptimalDecompose(CheckDecompositions):
Expand Down

0 comments on commit f15d758

Please sign in to comment.