Circuit transpiler#
For various reasons, we may want to convert a quantum circuit to another quantum circuit that is semantically equivalent.
For example, if a particular backend supports only a particular gate set, the gate set must be converted. Also, if the qubits are implemented in a particular topology, a conversion may be necessary to make the circuit viable. Converting a semantically equivalent redundant representation to a more concise representation may reduce the execution time of the circuit, the error rate, and the number of qubits.
These motivations can be broadly classified into two categories.
Backend (hardware) adaptation
Circuit optimization
QURI Parts provides a variety of circuit transpilers for these purposes. Users can also prepare a new transpiler by combining existing transpilers or implementing one from scratch. This tutorial will show you how to handle circuit transpilers with QURI Parts.
Prerequisite#
QURI Parts modules used in this tutorial: quri-parts-circuit
and quri-parts-core
. You can install them as follows:
[ ]:
!pip install "quri-parts"
Transpiler interface#
All transpilers in QURI Parts are CircuitTranspiler
and can convert NonParametricQuantumCircuit
to another NonParametricQuantumCircuit
by calling with the circuit as follows.
[2]:
from typing import Callable
from typing_extensions import TypeAlias
from quri_parts.circuit import NonParametricQuantumCircuit
from quri_parts.circuit.transpile import CircuitTranspiler
CircuitTranspiler: TypeAlias = Callable[
[NonParametricQuantumCircuit], NonParametricQuantumCircuit
]
[3]:
from quri_parts.circuit import QuantumCircuit
from quri_parts.circuit.transpile import RZSetTranspiler
from quri_parts.circuit.utils.circuit_drawer import draw_circuit
circuit = QuantumCircuit(3)
circuit.add_H_gate(2)
circuit.add_X_gate(0)
circuit.add_CNOT_gate(2, 1)
circuit.add_Z_gate(2)
print("original:")
draw_circuit(circuit)
transpiler = RZSetTranspiler()
transpiled_circuit = transpiler(circuit)
print("\ntranspiled:")
draw_circuit(transpiled_circuit)
original:
___
| X |
--|1 |-----------------
|___|
___
|CX |
----------|2 |---------
|___|
___ | ___
| H | | | Z |
--|0 |-----●-----|3 |-
|___| |___|
transpiled:
___
| X |
--|3 |---------------------------------
|___|
___
|CX |
--------------------------|4 |---------
|___|
___ ___ ___ | ___
|RZ | |sqX| |RZ | | |RZ |
--|0 |---|1 |---|2 |-----●-----|5 |-
|___| |___| |___| |___|
Multiple transpilers can be applied simply by lining up the transformations.
[4]:
from quri_parts.circuit.transpile import (
TOFFOLI2HTTdagCNOTTranspiler,
H2RZSqrtXTranspiler,
T2RZTranspiler,
Tdag2RZTranspiler,
)
circuit = QuantumCircuit(3)
circuit.add_TOFFOLI_gate(0, 1, 2)
circuit = TOFFOLI2HTTdagCNOTTranspiler()(circuit)
circuit = H2RZSqrtXTranspiler()(circuit)
circuit = T2RZTranspiler()(circuit)
circuit = Tdag2RZTranspiler()(circuit)
# draw_circuit(circuit)
It can also be written somewhat more easily by using SequentialTranspiler
by passing CircuitTranspiler
instances on creation.
[5]:
from quri_parts.circuit.transpile import SequentialTranspiler
circuit = QuantumCircuit(3)
circuit.add_TOFFOLI_gate(0, 1, 2)
transpiler = SequentialTranspiler([
TOFFOLI2HTTdagCNOTTranspiler(),
H2RZSqrtXTranspiler(),
T2RZTranspiler(),
Tdag2RZTranspiler(),
])
circuit = transpiler(circuit)
# draw_circuit(circuit)
If the gate transformations are exclusive, ParallelDecomposer
can also be used to make it more efficient. SequentialTranspiler
and ParallelDecomposer
can be nested since they also are CircuitTranspiler
.
[6]:
from quri_parts.circuit.transpile import ParallelDecomposer
circuit = QuantumCircuit(3)
circuit.add_TOFFOLI_gate(0, 1, 2)
transpiler = SequentialTranspiler([
TOFFOLI2HTTdagCNOTTranspiler(),
ParallelDecomposer([
H2RZSqrtXTranspiler(),
T2RZTranspiler(),
Tdag2RZTranspiler(),
]),
])
circuit = transpiler(circuit)
# draw_circuit(circuit)
Transpiler for backend adaptation#
Gate set conversion#
When a circuit is executed on a real machine in each backend, the gate set of the circuit is often limited to a few universal gates. Also, QURI Parts has high level gate representations such as multi-pauli gates, which are not supported by most backends. Therefore, the circuit must be tranpiled to convert gate set prior to the circuit execution on the backend.
When creating a SamplingBackend or converting a circuit, a default transpiler for each backend is automatically applied, but a user-specified transpiler can be used instead of the default one.
Complex gate decomposition
Module |
Transpiler |
Target gate |
Decomposed gate set |
Description |
---|---|---|---|---|
quri_parts.circuit.transpile |
PauliDecomposeTranspiler |
Pauli |
{X, Y, Z} |
Decompose Pauli gates into gate sequences including X, Y, and Z gates. |
quri_parts.circuit.transpile |
PauliRotationDecomposeTranspiler |
PauliRotation |
{H, RX, RZ, CNOT} |
Decompose PauliRotation gates into gate sequences including H, RX, RZ, and CNOT gates. |
quri_parts.circuit.transpile |
SingleQubitUnitaryMatrix2RYRZTranspiler |
UnitaryMatrix |
{RY, RZ} |
Decompose single qubit UnitaryMatrix gates into named gate sequences. |
quri_parts.circuit.transpile |
TwoQubitUnitaryMatrixKAKTranspiler |
UnitaryMatrix |
{H, S, RX, RY, RZ, CNOT} |
Decompose two qubit UnitaryMatrix gates into named gate sequences. |
Gate set conversion
Module |
Transpiler |
Target gate set |
Description |
---|---|---|---|
quri_parts.circuit.transpile |
RZSetTranspiler |
{X, SqrtX, RZ, CNOT} |
Gate set used in superconducting type equipment such as IBM Quantum via Qiskit. |
quri_parts.circuit.transpile |
RotationSetTranspiler |
{RX, RY, RZ, CNOT} |
Intermediate gate set for ion trap type equipment. |
quri_parts.circuit.transpile |
CliffordRZSetTranspiler |
{H, X, Y, Z, S, SqrtX, SqrtXdag, SqrtY, SqrtYdag, Sdag, RZ, CZ, CNOT} |
Clifford + RZ gate set. |
quri_parts.quantinuum.circuit.transpile |
QuantinuumSetTranspiler |
{U1q, RZ, ZZ, RZZ} |
Gate set for actual equipment of Quantinuum H1 and H2. |
quri_parts.ionq.circuit.transpile |
IonQSetTranspiler |
{GPi, GPi2, MS} |
Gate set for actual equipment of IonQ. |
Qubit mapping#
Real devices in the NISQ era are also constrained by the topology of the qubit. In most cases, these constraints are satisfied by the backend automatically transforming the circuit, but sometimes it is desirable to suppress the transformation by the backend and give an explicit mapping of the qubits.
Such qubit mapping can be specified by a dictionary when creating SamplingBackend
s, but you can also create QubitRemappingTranspiler
that performs the qubit mapping for given circuits.
[7]:
from quri_parts.circuit import H, X, CNOT
from quri_parts.circuit.transpile import QubitRemappingTranspiler
circuit = QuantumCircuit(3)
circuit.extend([H(0), X(1), CNOT(1, 2)])
print("original:")
draw_circuit(circuit)
circuit = QubitRemappingTranspiler({0: 2, 1: 0, 2: 1})(circuit)
print("\ntranspiled:")
draw_circuit(circuit)
original:
___
| H |
--|0 |---------
|___|
___
| X |
--|1 |-----●---
|___| |
_|_
|CX |
----------|2 |-
|___|
transpiled:
___
| X |
--|1 |-----●---
|___| |
_|_
|CX |
----------|2 |-
|___|
___
| H |
--|0 |---------
|___|
Transpiler for circuit optimization#
Quantum circuits may be converted to more concise circuits with equivalent action. In actual hardware, certain representations of equivalent circuits may reduce errors or decrease execution time. For example, in the NISQ era, the number of 2-qubit gates often has a significant impact on the error rate, and in the FTQC era, the number of T gates may affect the execution time of a circuit. Optimizing circuits based on these various criteria is another role expected of transpilers.
In QURI Parts, many optimization paths are currently private, but some are available and more will be public in the future.
Module |
Transpiler |
Type |
Description |
---|---|---|---|
quri_parts.circuit.transpile |
CliffordApproximationTranspiler |
Approximate |
Replace non-Clifford gates with approximate Clifford gate sequences. |
quri_parts.circuit.transpile |
IdentityInsertionTranspiler |
Equivalent |
Add Identity gates to qubits which have no gate acting on. |
quri_parts.circuit.transpile |
IdentityEliminationTranspiler |
Equivalent |
Remove all Identity gates. |
quri_parts.qiskit.circuit.transpile |
QiskitTranspiler |
Equivalent (Numerical error) |
Perform backend adaptation, gate set conversion, and circuit simplification using Qiskit’s capabilities. |
quri_parts.tket.circuit.transpile |
TketTranspiler |
Equivalent (Numerical error) |
Perfomr backend adaptation, gate set conversion, and circuit simplification using Tket’s capabilities. |
The most basic optimization paths for the rotation gates with parameters are available as follows.
Module |
Transpiler |
Type |
Description |
---|---|---|---|
quri_parts.circuit.transpile |
FuseRotationTranspiler |
Equivalent (Numerical error) |
Fuse consecutive rotation gates of the same kind. |
quri_parts.circuit.transpile |
NormalizeRotationTranspiler |
Equivalent (Numerical error) |
Normalize the rotation angle of the rotation gates to the specified range. |
quri_parts.circuit.transpile |
RX2NamedTranspiler |
Equivalent (Numerical error) |
Convert RX gate if the RX gate is equal to a named gate with no parameters. |
quri_parts.circuit.transpile |
RY2NamedTranspiler |
Equivalent (Numerical error) |
Convert RY gate if the RY gate is equal to a named gate with no parameters. |
quri_parts.circuit.transpile |
RZ2NamedTranspiler |
Equivalent (Numerical error) |
Convert RZ gate if the RZ gate is equal to a named gate with no parameters. |
Define your original transpilers#
As explained above, a transpiler chained by SequentialTranspiler
or ParallellDecomposer
is itself a CircuitTranspiler
and can be used like other transpilers. In addition, any callable object with an interface of CircuitTranspiler
can act as a transpiler, whether it is a user defined function or a class.
[8]:
def transpiler(circuit: NonParametricQuantumCircuit) -> NonParametricQuantumCircuit:
...
When defining the original transpiler as a class, CircuitTranspilerProtocol
is defined as an abstract base class that satisfies the properties CircuitTranspiler
and can be inherited.
[9]:
from quri_parts.circuit.transpile import CircuitTranspilerProtocol
class Transpiler(CircuitTranspilerProtocol):
def __call__(self, circuit: NonParametricQuantumCircuit) -> NonParametricQuantumCircuit:
...
GateDecomposer
and GateKindDecomposer
are available for transpilers that convert a specific type of gates in a circuit to some gate sequences (e.g., a transpiler for converting gate sets). GateDecomposer
can be used to create a new transpiler by writing only the target gate conditions and the transformation of a target gate into a gate sequence. GateKindDecomposer
is simillar to GateDecomposer
but it require gate names as target gate conditions.
[10]:
from collections.abc import Sequence
from quri_parts.circuit import QuantumGate, gate_names
from quri_parts.circuit.transpile import GateDecomposer, GateKindDecomposer
class S0toTTranspiler(GateDecomposer):
def is_target_gate(self, gate: QuantumGate) -> bool:
return gate.target_indices[0] == 0 and gate.name == gate_names.S
def decompose(self, gate: QuantumGate) -> Sequence[QuantumGate]:
target = gate.target_indices[0]
return [gate.T(target), gate.T(target)]
class AnyStoTTranspiler(GateKindDecomposer):
def target_gate_names(self) -> Sequence[str]:
return [gate_names.S]
def decompose(self, gate: QuantumGate) -> Sequence[QuantumGate]:
target = gate.target_indices[0]
return [gate.T(target), gate.T(target)]