Add DAGCircuit output option to OneQubitEulerDecomposer (#9188)

* Add DAGCircuit output option to OneQubitEulerDecomposer

This commit adds a new option to the euler one qubit decomposer class,
use_dag, which when set to True will return a DAGCircuit object for the
decomposed matrix instead of a QuantumCircuit object. This is useful for
transpiler passes that call the decomposer so that they don't have to
convert from a circuit to a dag. The passes that use the decomposer are
also updated to use this new option.

This was originally attempted before in #5926 but was abandoned because
at the time there was no real performance improvement from doing this.
However, since then this class has changed quite a bit, and after #9185
the overhead of conversion between QuantumCircuit and DAGCircuit is a
more critical component of the runtime performance of the 1 qubit
optimization pass. By operating natively in DAGCircuit from the
decomposer we're able to remove this overhead and directly substitute
the output of the decomposer in the pass.

* Use a dataclass for internal gate list and phase tracking

This commit updates the internal psx circuit generator function to pass
a dataclass with a field for the gates and phase instead of using 2
lists. This provides a cleaner interface for using a mutable reference
between the different functions used to add gates to the circuit.

* Directly combine op node lists in optimize_1q_commutation
This commit is contained in:
Matthew Treinish 2022-12-02 17:25:55 -05:00 committed by GitHub
parent 1dce04bdcd
commit cace20b513
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 153 additions and 119 deletions

View File

@ -13,11 +13,14 @@
"""
Decompose a single-qubit unitary via Euler angles.
"""
from dataclasses import dataclass
from typing import List
import numpy as np
from qiskit._accelerate import euler_one_qubit_decomposer
from qiskit.circuit.quantumcircuit import QuantumCircuit
from qiskit.circuit.quantumregister import QuantumRegister
from qiskit.circuit.quantumregister import Qubit
from qiskit.circuit.library.standard_gates import (
UGate,
PhaseGate,
@ -31,9 +34,9 @@ from qiskit.circuit.library.standard_gates import (
SXGate,
XGate,
)
from qiskit.circuit.gate import Gate
from qiskit.exceptions import QiskitError
from qiskit.quantum_info.operators.predicates import is_unitary_matrix
from qiskit._accelerate import euler_one_qubit_decomposer
DEFAULT_ATOL = 1e-12
@ -114,7 +117,7 @@ class OneQubitEulerDecomposer:
:math:`R\left(\theta+\pi,\frac{\pi}{2}-\lambda\right)`
"""
def __init__(self, basis="U3"):
def __init__(self, basis="U3", use_dag=False):
"""Initialize decomposer
Supported bases are: 'U', 'PSX', 'ZSXX', 'ZSX', 'U321', 'U3', 'U1X', 'RR', 'ZYZ', 'ZXZ',
@ -122,11 +125,33 @@ class OneQubitEulerDecomposer:
Args:
basis (str): the decomposition basis [Default: 'U3']
use_dag (bool): If true the output from calls to the decomposer
will be a :class:`~qiskit.dagcircuit.DAGCircuit` object instead of
:class:`~qiskit.circuit.QuantumCircuit`.
Raises:
QiskitError: If input basis is not recognized.
"""
self.basis = basis # sets: self._basis, self._params, self._circuit
self.use_dag = use_dag
def build_circuit(self, gates, global_phase):
"""Return the circuit or dag object from a list of gates."""
qr = [Qubit()]
if self.use_dag:
from qiskit.dagcircuit import dagcircuit
dag = dagcircuit.DAGCircuit()
dag.global_phase = global_phase
dag.add_qubits(qr)
for gate in gates:
dag.apply_operation_back(gate, [qr[0]])
return dag
else:
circuit = QuantumCircuit(qr, global_phase=global_phase)
for gate in gates:
circuit._append(gate, [qr[0]], [])
return circuit
def __call__(self, unitary, simplify=True, atol=DEFAULT_ATOL):
"""Decompose single qubit gate into a circuit.
@ -240,8 +265,8 @@ class OneQubitEulerDecomposer:
theta, phi, lam, phase = OneQubitEulerDecomposer._params_zyz(mat)
return theta, phi, lam, phase - 0.5 * (theta + phi + lam)
@staticmethod
def _circuit_kak(
self,
theta,
phi,
lam,
@ -275,8 +300,7 @@ class OneQubitEulerDecomposer:
QuantumCircuit: The assembled circuit.
"""
gphase = phase - (phi + lam) / 2
qr = QuantumRegister(1, "qr")
circuit = QuantumCircuit(qr)
circuit = []
if not simplify:
atol = -1.0
# Early return for the middle-gate-free case
@ -288,10 +312,9 @@ class OneQubitEulerDecomposer:
lam = _mod_2pi(lam, atol)
if abs(lam) > atol:
circuit._append(k_gate(lam), [qr[0]], [])
circuit.append(k_gate(lam))
gphase += lam / 2
circuit.global_phase = gphase
return circuit
return self.build_circuit(circuit, gphase)
if abs(theta - np.pi) < atol:
gphase += phi
lam, phi = lam - phi, 0
@ -302,14 +325,13 @@ class OneQubitEulerDecomposer:
lam = _mod_2pi(lam, atol)
if abs(lam) > atol:
gphase += lam / 2
circuit._append(k_gate(lam), [qr[0]], [])
circuit._append(a_gate(theta), [qr[0]], [])
circuit.append(k_gate(lam))
circuit.append(a_gate(theta))
phi = _mod_2pi(phi, atol)
if abs(phi) > atol:
gphase += phi / 2
circuit._append(k_gate(phi), [qr[0]], [])
circuit.global_phase = gphase
return circuit
circuit.append(k_gate(phi))
return self.build_circuit(circuit, gphase)
def _circuit_zyz(
self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL, allow_non_canonical=True
@ -371,167 +393,158 @@ class OneQubitEulerDecomposer:
a_gate=RYGate,
)
@staticmethod
def _circuit_u3(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL):
qr = QuantumRegister(1, "qr")
circuit = QuantumCircuit(qr, global_phase=phase)
def _circuit_u3(self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL):
circuit = []
phi = _mod_2pi(phi, atol)
lam = _mod_2pi(lam, atol)
if not simplify or abs(theta) > atol or abs(phi) > atol or abs(lam) > atol:
circuit._append(U3Gate(theta, phi, lam), [qr[0]], [])
return circuit
circuit.append(U3Gate(theta, phi, lam))
return self.build_circuit(circuit, phase)
@staticmethod
def _circuit_u321(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL):
qr = QuantumRegister(1, "qr")
circuit = QuantumCircuit(qr, global_phase=phase)
def _circuit_u321(self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL):
circuit = []
if not simplify:
atol = -1.0
if abs(theta) < atol:
tot = _mod_2pi(phi + lam, atol)
if abs(tot) > atol:
circuit._append(U1Gate(tot), [qr[0]], [])
circuit.append(U1Gate(tot))
elif abs(theta - np.pi / 2) < atol:
circuit._append(U2Gate(_mod_2pi(phi, atol), _mod_2pi(lam, atol)), [qr[0]], [])
circuit.append(U2Gate(_mod_2pi(phi, atol), _mod_2pi(lam, atol)))
else:
circuit._append(U3Gate(theta, _mod_2pi(phi, atol), _mod_2pi(lam, atol)), [qr[0]], [])
return circuit
circuit.append(U3Gate(theta, _mod_2pi(phi, atol), _mod_2pi(lam, atol)))
return self.build_circuit(circuit, phase)
@staticmethod
def _circuit_u(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL):
qr = QuantumRegister(1, "qr")
circuit = QuantumCircuit(qr, global_phase=phase)
def _circuit_u(self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL):
circuit = []
if not simplify:
atol = -1.0
phi = _mod_2pi(phi, atol)
lam = _mod_2pi(lam, atol)
if abs(theta) > atol or abs(phi) > atol or abs(lam) > atol:
circuit._append(UGate(theta, phi, lam), [qr[0]], [])
return circuit
circuit.append(UGate(theta, phi, lam))
return self.build_circuit(circuit, phase)
@staticmethod
def _circuit_psx_gen(theta, phi, lam, phase, atol, pfun, xfun, xpifun=None):
def _circuit_psx_gen(self, theta, phi, lam, phase, atol, pfun, xfun, xpifun=None):
"""
Generic X90, phase decomposition
NOTE: `pfun` is responsible for eliding gates where appropriate (e.g., at angle value 0).
"""
qr = QuantumRegister(1, "qr")
circuit = QuantumCircuit(qr, global_phase=phase)
circuit = _PSXGenCircuit([], phase)
# Early return for zero SX decomposition
if np.abs(theta) < atol:
pfun(circuit, qr, lam + phi)
return circuit
pfun(circuit, lam + phi)
return self.build_circuit(circuit.circuit, circuit.phase)
# Early return for single SX decomposition
if abs(theta - np.pi / 2) < atol:
pfun(circuit, qr, lam - np.pi / 2)
xfun(circuit, qr)
pfun(circuit, qr, phi + np.pi / 2)
return circuit
pfun(circuit, lam - np.pi / 2)
xfun(circuit)
pfun(circuit, phi + np.pi / 2)
return self.build_circuit(circuit.circuit, circuit.phase)
# General double SX decomposition
if abs(theta - np.pi) < atol:
circuit.global_phase += lam
circuit.phase += lam
phi, lam = phi - lam, 0
if abs(_mod_2pi(lam + np.pi)) < atol or abs(_mod_2pi(phi)) < atol:
lam, theta, phi = lam + np.pi, -theta, phi + np.pi
circuit.global_phase -= theta
circuit.phase -= theta
# Shift theta and phi to turn the decomposition from
# RZ(phi).RY(theta).RZ(lam) = RZ(phi).RX(-pi/2).RZ(theta).RX(pi/2).RZ(lam)
# into RZ(phi+pi).SX.RZ(theta+pi).SX.RZ(lam) .
theta, phi = theta + np.pi, phi + np.pi
circuit.global_phase -= np.pi / 2
circuit.phase -= np.pi / 2
# Emit circuit
pfun(circuit, qr, lam)
pfun(circuit, lam)
if xpifun and abs(_mod_2pi(theta)) < atol:
xpifun(circuit, qr)
xpifun(circuit)
else:
xfun(circuit, qr)
pfun(circuit, qr, theta)
xfun(circuit, qr)
pfun(circuit, qr, phi)
xfun(circuit)
pfun(circuit, theta)
xfun(circuit)
pfun(circuit, phi)
return circuit
return self.build_circuit(circuit.circuit, circuit.phase)
@staticmethod
def _circuit_psx(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL):
def _circuit_psx(self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL):
if not simplify:
atol = -1.0
def fnz(circuit, qr, phi):
def fnz(circuit, phi):
phi = _mod_2pi(phi, atol)
if abs(phi) > atol:
circuit._append(PhaseGate(phi), [qr[0]], [])
circuit.circuit.append(PhaseGate(phi))
def fnx(circuit, qr):
circuit._append(SXGate(), [qr[0]], [])
def fnx(circuit):
circuit.circuit.append(SXGate())
return OneQubitEulerDecomposer._circuit_psx_gen(theta, phi, lam, phase, atol, fnz, fnx)
return self._circuit_psx_gen(theta, phi, lam, phase, atol, fnz, fnx)
@staticmethod
def _circuit_zsx(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL):
def _circuit_zsx(self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL):
if not simplify:
atol = -1.0
def fnz(circuit, qr, phi):
def fnz(circuit, phi):
phi = _mod_2pi(phi, atol)
if abs(phi) > atol:
circuit._append(RZGate(phi), [qr[0]], [])
circuit.global_phase += phi / 2
circuit.circuit.append(RZGate(phi))
circuit.phase += phi / 2
def fnx(circuit, qr):
circuit._append(SXGate(), [qr[0]], [])
def fnx(circuit):
circuit.circuit.append(SXGate())
return OneQubitEulerDecomposer._circuit_psx_gen(theta, phi, lam, phase, atol, fnz, fnx)
return self._circuit_psx_gen(theta, phi, lam, phase, atol, fnz, fnx)
@staticmethod
def _circuit_u1x(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL):
def _circuit_u1x(self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL):
if not simplify:
atol = -1.0
def fnz(circuit, qr, phi):
def fnz(circuit, phi):
phi = _mod_2pi(phi, atol)
if abs(phi) > atol:
circuit._append(U1Gate(phi), [qr[0]], [])
circuit.circuit.append(U1Gate(phi))
def fnx(circuit, qr):
circuit.global_phase += np.pi / 4
circuit._append(RXGate(np.pi / 2), [qr[0]], [])
def fnx(circuit):
circuit.phase += np.pi / 4
circuit.circuit.append(RXGate(np.pi / 2))
return OneQubitEulerDecomposer._circuit_psx_gen(theta, phi, lam, phase, atol, fnz, fnx)
return self._circuit_psx_gen(theta, phi, lam, phase, atol, fnz, fnx)
@staticmethod
def _circuit_zsxx(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL):
def _circuit_zsxx(self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL):
if not simplify:
atol = -1.0
def fnz(circuit, qr, phi):
def fnz(circuit, phi):
phi = _mod_2pi(phi, atol)
if abs(phi) > atol:
circuit._append(RZGate(phi), [qr[0]], [])
circuit.global_phase += phi / 2
circuit.circuit.append(RZGate(phi))
circuit.phase += phi / 2
def fnx(circuit, qr):
circuit._append(SXGate(), [qr[0]], [])
def fnx(circuit):
circuit.circuit.append(SXGate())
def fnxpi(circuit, qr):
circuit._append(XGate(), [qr[0]], [])
def fnxpi(circuit):
circuit.circuit.append(XGate())
return OneQubitEulerDecomposer._circuit_psx_gen(
theta, phi, lam, phase, atol, fnz, fnx, fnxpi
)
return self._circuit_psx_gen(theta, phi, lam, phase, atol, fnz, fnx, fnxpi)
@staticmethod
def _circuit_rr(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL):
qr = QuantumRegister(1, "qr")
circuit = QuantumCircuit(qr, global_phase=phase)
def _circuit_rr(self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL):
circuit = []
if not simplify:
atol = -1.0
if abs(theta) < atol and abs(phi) < atol and abs(lam) < atol:
return circuit
return self.build_circuit(circuit, phase)
if abs(theta - np.pi) > atol:
circuit._append(RGate(theta - np.pi, _mod_2pi(np.pi / 2 - lam, atol)), [qr[0]], [])
circuit._append(RGate(np.pi, _mod_2pi(0.5 * (phi - lam + np.pi), atol)), [qr[0]], [])
return circuit
circuit.append(RGate(theta - np.pi, _mod_2pi(np.pi / 2 - lam, atol)))
circuit.append(RGate(np.pi, _mod_2pi(0.5 * (phi - lam + np.pi), atol)))
return self.build_circuit(circuit, phase)
@dataclass
class _PSXGenCircuit:
__slots__ = ("circuit", "phase")
circuit: List[Gate]
phase: float
def _mod_2pi(angle: float, atol: float = 0):

View File

@ -16,9 +16,9 @@ from copy import copy
import logging
from collections import deque
from qiskit.circuit import QuantumCircuit
from qiskit.dagcircuit import DAGCircuit
from qiskit.circuit import QuantumRegister
from qiskit.circuit.library.standard_gates import CXGate, RZXGate
from qiskit.converters import circuit_to_dag
from qiskit.dagcircuit import DAGOpNode
from qiskit.transpiler.basepasses import TransformationPass
from qiskit.transpiler.passes.optimization.optimize_1q_decomposition import (
@ -157,18 +157,19 @@ class Optimize1qGatesSimpleCommutation(TransformationPass):
NOTE: Returns None when resynthesis is not possible.
"""
if len(new_run) == 0:
return QuantumCircuit(1)
dag = DAGCircuit()
dag.add_qreg(QuantumRegister(1))
return dag
return self._optimize1q._resynthesize_run(new_run)
@staticmethod
def _replace_subdag(dag, old_run, new_circ):
def _replace_subdag(dag, old_run, new_dag):
"""
Replaces a nonempty sequence `old_run` of `DAGNode`s, assumed to be a complete chain in
`dag`, with the circuit `new_circ`.
"""
new_dag = circuit_to_dag(new_circ)
node_map = dag.substitute_node_with_dag(old_run[0], new_dag)
for node in old_run[1:]:
@ -219,11 +220,7 @@ class Optimize1qGatesSimpleCommutation(TransformationPass):
if self._optimize1q._substitution_checks(
dag,
(preceding_run or []) + run + (succeeding_run or []),
(
(new_preceding_run or QuantumCircuit(1)).data
+ (new_run or QuantumCircuit(1)).data
+ (new_succeeding_run or QuantumCircuit(1)).data
),
new_preceding_run.op_nodes() + new_run.op_nodes() + new_succeeding_run.op_nodes(),
self._optimize1q._basis_gates,
qubit_indices[run[0].qargs[0]],
):

View File

@ -19,7 +19,6 @@ import numpy as np
from qiskit.transpiler.basepasses import TransformationPass
from qiskit.transpiler.passes.utils import control_flow
from qiskit.quantum_info.synthesis import one_qubit_decompose
from qiskit.converters import circuit_to_dag
logger = logging.getLogger(__name__)
@ -135,15 +134,14 @@ class Optimize1qGatesDecomposition(TransformationPass):
qubit_indices = {bit: index for index, bit in enumerate(dag.qubits)}
for run in runs:
qubit = qubit_indices[run[0].qargs[0]]
new_circ = self._resynthesize_run(run, qubit)
new_dag = self._resynthesize_run(run, qubit)
if self._target is None:
basis = self._basis_gates
else:
basis = self._target.operation_names_for_qargs((qubit,))
if new_circ is not None and self._substitution_checks(dag, run, new_circ, basis, qubit):
new_dag = circuit_to_dag(new_circ)
if new_dag is not None and self._substitution_checks(dag, run, new_dag, basis, qubit):
dag.substitute_node_with_dag(run[0], new_dag)
# Delete the other nodes in the run
for current_node in run[1:]:
@ -156,14 +154,16 @@ def _possible_decomposers(basis_set):
decomposers = []
if basis_set is None:
decomposers = [
one_qubit_decompose.OneQubitEulerDecomposer(basis)
one_qubit_decompose.OneQubitEulerDecomposer(basis, use_dag=True)
for basis in one_qubit_decompose.ONE_QUBIT_EULER_BASIS_GATES
]
else:
euler_basis_gates = one_qubit_decompose.ONE_QUBIT_EULER_BASIS_GATES
for euler_basis_name, gates in euler_basis_gates.items():
if set(gates).issubset(basis_set):
decomposer = one_qubit_decompose.OneQubitEulerDecomposer(euler_basis_name)
decomposer = one_qubit_decompose.OneQubitEulerDecomposer(
euler_basis_name, use_dag=True
)
decomposers.append(decomposer)
return decomposers
@ -177,7 +177,10 @@ def _error(circuit, target, qubit):
of circuit as a weak proxy for error.
"""
if target is None:
return len(circuit)
if isinstance(circuit, list):
return len(circuit)
else:
return len(circuit._multi_graph) - 2
else:
if isinstance(circuit, list):
gate_fidelities = [
@ -185,11 +188,16 @@ def _error(circuit, target, qubit):
]
else:
gate_fidelities = [
1 - getattr(target[inst.operation.name].get((qubit,)), "error", 0.0)
for inst in circuit
1 - getattr(target[inst.op.name].get((qubit,)), "error", 0.0)
for inst in circuit.op_nodes()
]
gate_error = 1 - np.product(gate_fidelities)
if gate_error == 0.0:
return -100 + len(circuit) # prefer shorter circuits among those with zero error
if isinstance(circuit, list):
return -100 + len(circuit)
else:
return -100 + len(
circuit._multi_graph
) # prefer shorter circuits among those with zero error
else:
return gate_error

View File

@ -0,0 +1,11 @@
---
features:
- |
Added a new keyword argument, ``use_dag`` to the constructor for the
:class:`~.OneQubitEulerDecomposer` class. When ``use_dag`` is set to
``True`` the output from the decomposer will be a :class:`~.DAGCircuit`
object instead of :class:`~.QuantumCircuit` object. This is useful
for transpiler passes that use :class:`~.OneQubitEulerDecomposer` (such
as :class:`~.Optimize1qGatesDecomposition`) as working directly with
a :class:`~.DAGCircuit` avoids the overhead of converting between
:class:`~.QuantumCircuit` and :class:`~.DAGCircuit`.

View File

@ -39,6 +39,7 @@ from qiskit.transpiler.passes.optimization.optimize_1q_decomposition import _err
from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel
from qiskit.quantum_info import Operator
from qiskit.test import QiskitTestCase
from qiskit.converters import circuit_to_dag
θ = Parameter("θ")
@ -157,7 +158,9 @@ class TestOptimize1qGatesDecomposition(QiskitTestCase):
passmanager = PassManager()
passmanager.append(Optimize1qGatesDecomposition(target=target))
result = passmanager.run(circuit)
self.assertLess(_error(result, target, 0), _error(circuit, target, 0))
self.assertLess(
_error(circuit_to_dag(result), target, 0), _error(circuit_to_dag(circuit), target, 0)
)
def test_optimize_error_over_target_2(self):
"""U is re-written as ZYZ, which is cheaper according to target."""
@ -169,7 +172,9 @@ class TestOptimize1qGatesDecomposition(QiskitTestCase):
passmanager = PassManager()
passmanager.append(Optimize1qGatesDecomposition(target=target))
result = passmanager.run(circuit)
self.assertLess(_error(result, target, 0), _error(circuit, target, 0))
self.assertLess(
_error(circuit_to_dag(result), target, 0), _error(circuit_to_dag(circuit), target, 0)
)
def test_optimize_error_over_target_3(self):
"""U is shorter than RZ-RY-RZ or RY-RZ-RY so use it when no error given."""