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"
Overview¶
As an example, let’s frist set up the following by circuit and apply the RZ set transpiler. The RZ set transpiler is a transpiler that converts the circuit to one that contains only X, SqrtX, CNOT, and RZ gates. This is done as follows.
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 |-
|___| |___| |___| |___|
The RZSetTranspiler here is a transpiler made up of multiple simpler transpilers. The goal of this tutorial would be to introduce the transpiler interface and explain how to build customized transpilers.
Transpiler interface¶
All transpilers in QURI Parts are CircuitTranspiler and can convert NonParametricQuantumCircuit to another NonParametricQuantumCircuit.
from typing import Callable
from typing_extensions import TypeAlias
from quri_parts.circuit import NonParametricQuantumCircuit
CircuitTranspiler: TypeAlias = Callable[
[NonParametricQuantumCircuit], NonParametricQuantumCircuit
]
There are multiple types of transpiler to perform different kinds of transpilations. They are:
GateDecomposer: A transpiler that decomposes a gate if the gate meets specific condition set by the gate decomposer.GateKindDecomposer: A transpiler that decomposes a gate for a specific type of gate. In other words, it is aGateDecomposerthat checks if the gate name matches with the target gate’s name.ParallelDecomposer: A transpiler that composes multipleGateKindDecomposers whose target gates are exclusive of each other. It iterates through the circuit once and decomposes all the type of gates set by theParallelDecomposer.SequentialTranspiler: A transpiler that composes multiple transpilers and performs the transpilation in sequence.
Gate kind decomposer and gate decomposer¶
We first introduce 2 types of basic transpilers that convert gates: GateKindDecomposer and GateDecomposer.
GateDecomposer¶
As memtioned above a GateDecomposer is a transpiler that decomposes a gate if the gate meets certain conditions. In QURI Parts, two concrete implemetations of them are provided
SingleQubitUnitaryMatrix2RYRZTranspilerTwoQubitUnitaryMatrixKAKTranspiler
As the names suggest, these gate decomposers decomposes the gate if the gate is a unitary matrix gate acting on 1 qubit or 2 qubits respectively. Let’s look at an example with SingleQubitUnitaryMatrix2RYRZTranspiler.
from quri_parts.circuit.transpile import SingleQubitUnitaryMatrix2RYRZTranspiler
from scipy.stats import unitary_group
single_qubit_matrix = unitary_group.rvs(2)
double_qubit_matrix = unitary_group.rvs(4)
circuit = QuantumCircuit(2)
circuit.add_UnitaryMatrix_gate([0], single_qubit_matrix)
circuit.add_UnitaryMatrix_gate([0, 1], double_qubit_matrix)
print("original circuit:")
draw_circuit(circuit)
transpiler = SingleQubitUnitaryMatrix2RYRZTranspiler()
transpiled_circuit = transpiler(circuit)
print("")
print("transpiled circuit:")
draw_circuit(transpiled_circuit)
original circuit:
___ ___
|Mat| |Mat|
--|0 |---|1 |-
|___| | |
| |
| |
----------| |-
|___|
transpiled circuit:
___ ___ ___ ___
|RZ | |RY | |RZ | |Mat|
--|0 |---|1 |---|2 |---|3 |-
|___| |___| |___| | |
| |
| |
--------------------------| |-
|___|
From this example above, we see that while both gates are of type UnitaryMatrix, but the GateDecomposer SingleQubitUnitaryMatrix2RYRZTranspiler only takes effect on UnitaryMatrix gates acting on a single qubit, thus leaving the 2-qubit unitary matrix gate untouched during the transpilation. A GateDecomposer provides a is_target_gate to check if a gate is to be converted:
print("Single qubit unitary gate should be converted:", transpiler.is_target_gate(circuit.gates[0]))
print("Double qubit unitary gate should be converted:", transpiler.is_target_gate(circuit.gates[1]))
Single qubit unitary gate should be converted: True
Double qubit unitary gate should be converted: False
GateKindDecomposer¶
The other type of basic gate transpiler is the GateKindDecomposer. It is a subtype of a GateDecomposer that checks if a gate’s name matches that of the gate we want to transpile. It does not perform checks on other attributes of a QuantumGate. QURI Parts provides an enormous amount of them in the quri_parts.circuit.transpile.gate_kind_decomposer module. We suggest you to refer to the API page for the list of GateKindDecomposer we provide.
As an exmaple, we introduce the H2RZSqrtXTranspiler that transpiles Hadamard gates to sequence of \(R_z\) and \(\sqrt{X}\) gates.
from quri_parts.circuit.transpile import H2RZSqrtXTranspiler
circuit = QuantumCircuit(2)
circuit.add_H_gate(0)
circuit.add_X_gate(1)
print("original circuit:")
draw_circuit(circuit)
transpiler = H2RZSqrtXTranspiler()
transpiled_circuit = transpiler(circuit)
print("")
print("transpiled circuit:")
draw_circuit(transpiled_circuit)
original circuit:
___
| H |
--|0 |-
|___|
___
| X |
--|1 |-
|___|
transpiled circuit:
___ ___ ___
|RZ | |sqX| |RZ |
--|0 |---|1 |---|2 |-
|___| |___| |___|
___
| X |
--|3 |-----------------
|___|
Sequential transpilers¶
Multiple transpilers can be applied simply by lining up the transformations. Here, we use a circuit made of a single Toffoli gate as an example. Here we make the following sequence of transpilations
Transpiler 1: \(\text{Toffoli}\) \(\rightarrow\) (\(\text{H}\), \(\text{T}\), \(\text{T}^{\dagger}\), \(\text{CNOT}\))
Transpiler 2: \(\text{H}\) \(\rightarrow\) (\(R_Z\), \(\sqrt{\text{X}}\))
Transpiler 3: \(\text{T}\) \(\rightarrow\) \(R_Z\)
Transpiler 4: \(\text{T}^{\dagger}\) \(\rightarrow\) \(R_Z\)
These transpilers are already provided by QURI Parts. Let’s demonstrate how to use them:
from quri_parts.circuit.transpile import (
TOFFOLI2HTTdagCNOTTranspiler,
H2RZSqrtXTranspiler,
T2RZTranspiler,
Tdag2RZTranspiler,
)
circuit = QuantumCircuit(3)
circuit.add_TOFFOLI_gate(0, 1, 2)
print("original:")
draw_circuit(circuit, line_length=120)
circuit = TOFFOLI2HTTdagCNOTTranspiler()(circuit)
circuit = H2RZSqrtXTranspiler()(circuit)
circuit = T2RZTranspiler()(circuit)
circuit = Tdag2RZTranspiler()(circuit)
print("")
print("Sequential transpiled:")
draw_circuit(circuit, line_length=120)
original:
----●---
|
|
|
----●---
|
_|_
|TOF|
--|0 |-
|___|
Sequential transpiled:
___
|RZ |
--------------------------------------------●-------------------------------●-------●-----|16 |-----●-----------
| | | |___| |
| ___ | _|_ ___ _|_
| |RZ | | |CX | |RZ | |CX |
----------------------------●---------------|---------------●-----|10 |-----|-----|15 |---|17 |---|18 |---------
| | | |___| | |___| |___| |___|
___ ___ ___ _|_ ___ _|_ ___ _|_ ___ _|_ ___ ___ ___ ___
|RZ | |sqX| |RZ | |CX | |RZ | |CX | |RZ | |CX | |RZ | |CX | |RZ | |RZ | |sqX| |RZ |
--|0 |---|1 |---|2 |---|3 |---|4 |---|5 |---|6 |---|7 |---|8 |---|9 |---|11 |---|12 |---|13 |---|14 |-
|___| |___| |___| |___| |___| |___| |___| |___| |___| |___| |___| |___| |___| |___|
It can also be written somewhat more easily by using SequentialTranspiler by passing CircuitTranspiler instances on creation.
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, line_length=120)
___
|RZ |
--------------------------------------------●-------------------------------●-------●-----|16 |-----●-----------
| | | |___| |
| ___ | _|_ ___ _|_
| |RZ | | |CX | |RZ | |CX |
----------------------------●---------------|---------------●-----|10 |-----|-----|15 |---|17 |---|18 |---------
| | | |___| | |___| |___| |___|
___ ___ ___ _|_ ___ _|_ ___ _|_ ___ _|_ ___ ___ ___ ___
|RZ | |sqX| |RZ | |CX | |RZ | |CX | |RZ | |CX | |RZ | |CX | |RZ | |RZ | |sqX| |RZ |
--|0 |---|1 |---|2 |---|3 |---|4 |---|5 |---|6 |---|7 |---|8 |---|9 |---|11 |---|12 |---|13 |---|14 |-
|___| |___| |___| |___| |___| |___| |___| |___| |___| |___| |___| |___| |___| |___|
Parallel decomposers¶
It is often the case that we want to transpile multiple types of gates at once. While sequential transpilers can do the job, it is more efficient if we compose multiple GateKindDecomposers into a single ParallelDecomposer. We should re-emphasize that a GateKindDecomposer is a transpiler that transpile a gate based on what type of gate it is. Hence the gate transformations that makes up a ParallelDecomposer should act on gates that are exclusive of each other.
We revisit the last example where we transpile a Toffoli gate into smaller gates. In the last example, we used a sequential transpiler that made up of 4 transpilers. Thus, the circuit was iterated over 4 times. However, if we look at transpilers 2, 3 and 4, the gates that they act on are distinct. Also, any of the output gate sets will not be further transpiled by any other transpilers under consideration. That means it is more desirable to merge the last 3 transpilers into a single ParallelDecomposer. This way, the transpilation can be done with 2 iterations to the circuit. To be more explcit, the steps are:
Step 1:
TOFFOLI2HTTdagCNOTTranspilerStep 2: A
ParallelDecomposerthat consists of:H2RZSqrtXTranspilerT2RZTranspilerTdag2RZTranspiler
Here we show how we can nest SequentialTranspiler and ParallelDecomposer to make a new CircuitTranspiler.
from quri_parts.circuit.transpile import ParallelDecomposer, SequentialTranspiler
circuit = QuantumCircuit(3)
circuit.add_TOFFOLI_gate(0, 1, 2)
print("original circuit:")
draw_circuit(circuit)
transpiler = SequentialTranspiler([
TOFFOLI2HTTdagCNOTTranspiler(),
ParallelDecomposer([
H2RZSqrtXTranspiler(),
T2RZTranspiler(),
Tdag2RZTranspiler(),
]),
])
circuit = transpiler(circuit)
print("\n")
print("transpiled circuit:")
draw_circuit(circuit, line_length=200)
original circuit:
----●---
|
|
|
----●---
|
_|_
|TOF|
--|0 |-
|___|
transpiled circuit:
___
|RZ |
--------------------------------------------●-------------------------------●-------●-----|16 |-----●-----------
| | | |___| |
| ___ | _|_ ___ _|_
| |RZ | | |CX | |RZ | |CX |
----------------------------●---------------|---------------●-----|10 |-----|-----|15 |---|17 |---|18 |---------
| | | |___| | |___| |___| |___|
___ ___ ___ _|_ ___ _|_ ___ _|_ ___ _|_ ___ ___ ___ ___
|RZ | |sqX| |RZ | |CX | |RZ | |CX | |RZ | |CX | |RZ | |CX | |RZ | |RZ | |sqX| |RZ |
--|0 |---|1 |---|2 |---|3 |---|4 |---|5 |---|6 |---|7 |---|8 |---|9 |---|11 |---|12 |---|13 |---|14 |-
|___| |___| |___| |___| |___| |___| |___| |___| |___| |___| |___| |___| |___| |___|
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 |
|---|---|---|---|
quri_parts.circuit.transpile |
PauliDecomposeTranspiler |
Pauli |
{X, Y, Z} |
quri_parts.circuit.transpile |
PauliRotationDecomposeTranspiler |
PauliRotation |
{H, RX, RZ, CNOT} |
quri_parts.circuit.transpile |
SingleQubitUnitaryMatrix2RYRZTranspiler |
UnitaryMatrix |
{RY, RZ} |
quri_parts.circuit.transpile |
TwoQubitUnitaryMatrixKAKTranspiler |
UnitaryMatrix |
{H, S, RX, RY, RZ, CNOT} |
Gate set conversion¶
Module |
Transpiler |
Target gate |
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.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 SamplingBackends (see qubit mapping in sampling backends tutorial), but you can also create QubitRemappingTranspiler that performs the qubit mapping for given circuits.
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. |
Transpiling into fault-tolerant gate sets¶
Fault-tolerant architectures expose only a small, discrete set of native gates, so before resource estimation or execution a circuit must be rewritten into the chosen set. quri_parts.circuit.transpile ships ready-made transpilers for the common fault-tolerant gate sets; each takes a QuantumCircuit and returns an equivalent circuit built only from the target gates. The qsub-level counterparts that operate on a Sub are covered in the qsub transpiler tutorial.
Rotation decomposition into {H, S, T}¶
RZ2HSTTranspiler (from quri_parts.circuit.transpile.rz2hst) approximates every RZ rotation by a discrete {H, S, T} (plus X) sequence using the gridsynth algorithm, to within a target precision epsilon (default 1e-5). Because a discrete set cannot represent an arbitrary continuous rotation exactly, the rotation is approximated: a smaller epsilon yields a closer approximation and a longer, more T-heavy sequence. By default it uses the pure-Python pygridsynth package, installed with quri-parts-circuit; driver_cli is available when you instead want to use the external gridsynth command.
from collections import Counter
from quri_parts.circuit import QuantumCircuit
from quri_parts.circuit.transpile.rz2hst import RZ2HSTTranspiler
circuit = QuantumCircuit(2)
circuit.add_H_gate(0)
circuit.add_RZ_gate(0, 0.2)
circuit.add_CNOT_gate(0, 1)
circuit.add_RZ_gate(1, 0.5)
transpiled = RZ2HSTTranspiler(epsilon=1e-3)(circuit)
print(Counter(g.name for g in transpiled.gates))
Clifford+RZ¶
CliffordRZSetTranspiler rewrites a circuit into the full Clifford group plus the continuous RZ rotation ({Identity, H, X, Y, Z, SqrtX, SqrtXdag, SqrtY, SqrtYdag, S, Sdag, CNOT, CZ, SWAP, RZ}). Because RZ stays native, arbitrary single-qubit rotations such as RX/RY collapse onto RZ plus a few Clifford gates with no discrete-set approximation — a convenient intermediate target before committing to a Clifford+T-style basis. Its epsilon (default 1e-9) only controls when a near-Clifford angle is snapped to a named gate; non-Clifford angles are always kept exactly as RZ, so no gridsynth is needed.
from quri_parts.circuit.transpile import CliffordRZSetTranspiler
circuit = QuantumCircuit(2)
circuit.add_H_gate(0)
circuit.add_RX_gate(0, 0.5)
circuit.add_RY_gate(1, 0.7)
circuit.add_CNOT_gate(0, 1)
circuit.add_RZ_gate(1, 0.3)
transpiled = CliffordRZSetTranspiler()(circuit)
print(Counter(g.name for g in transpiled.gates))
STAR ({H, S, RZ, CNOT})¶
STARSetTranspiler targets the STAR architecture’s small native set {H, S, RZ, CNOT}. As with Clifford+RZ, RZ remains a native gate, so RX/RY are rewritten in terms of H, S, and RZ without any discrete {H, T} synthesis — there is no gridsynth step and no epsilon argument (an internal tolerance still governs rotation fusing and near-Clifford snapping). This is the key difference from a {Clifford, T} set, where arbitrary RZ angles must be approximated.
from quri_parts.circuit.transpile import STARSetTranspiler
circuit = QuantumCircuit(2)
circuit.add_H_gate(0)
circuit.add_RX_gate(0, 0.5)
circuit.add_RY_gate(1, 0.7)
circuit.add_CNOT_gate(0, 1)
circuit.add_RZ_gate(1, 0.3)
transpiled = STARSetTranspiler()(circuit)
print(Counter(g.name for g in transpiled.gates))
Clifford+T¶
CliffordTSetTranspiler targets the discrete, universal Clifford+T set ({Identity, H, X, Y, Z, SqrtX, SqrtXdag, SqrtY, SqrtYdag, S, Sdag, CNOT, CZ, SWAP, T, Tdag}). Clifford gates are cheap fault-tolerantly while T is expensive, so T-count is the standard cost metric. Every RZ is approximated into an {H, S, T} sequence with gridsynth, so no RZ remains. epsilon (default 1e-6) is the gridsynth synthesis precision. By default gridsynth runs through the pure-Python pygridsynth package installed with quri-parts-circuit; driver_cli is available for the external gridsynth command. The example below uses pi/4-multiple angles, which are numerically exact to within epsilon.
from math import pi
from quri_parts.circuit.transpile import CliffordTSetTranspiler
circuit = QuantumCircuit(2)
circuit.add_H_gate(0)
circuit.add_RZ_gate(0, pi / 4)
circuit.add_RZ_gate(1, -pi / 2)
circuit.add_CNOT_gate(0, 1)
circuit.add_RZ_gate(0, 3 * pi / 4)
transpiled = CliffordTSetTranspiler()(circuit)
print(Counter(g.name for g in transpiled.gates))
FTQC basic set ({H, S, T, CNOT})¶
The FTQC basic set {H, S, T, CNOT} is the Clifford gates H, S, CNOT plus the non-Clifford T; together they are universal, and T-count is the dominant cost. There is no dedicated named transpiler for it — you target it with the general GateSetConversionTranspiler, passing the gate names. pi/4-multiple RZ angles convert exactly; arbitrary angles are approximated with gridsynth in the same pass (precision epsilon, default 1e-9). The qsub-native FTQCSetTranspiler targets the same gate set via its own decomposition sequence.
from quri_parts.circuit import gate_names
from quri_parts.circuit.transpile import GateSetConversionTranspiler
circuit = QuantumCircuit(2)
circuit.add_H_gate(0)
circuit.add_RZ_gate(0, pi / 4)
circuit.add_RZ_gate(1, -pi / 2)
circuit.add_CNOT_gate(0, 1)
circuit.add_RZ_gate(0, 3 * pi / 4)
ftqc = GateSetConversionTranspiler(
[gate_names.H, gate_names.S, gate_names.T, gate_names.CNOT]
)
print(Counter(g.name for g in ftqc(circuit).gates))
An arbitrary gate set¶
GateSetConversionTranspiler is the general tool underlying the sets above: give it any list of target gate names and it rewrites a circuit to use only those, decomposing rotations such as RX/RY as needed. Its epsilon (default 1e-9) controls rotation approximation. How it is used depends on the target: keeping RZ in the set (as in {H, T, CNOT, RZ} below) keeps arbitrary Z rotations exact and needs no gridsynth, whereas a fully discrete set such as {H, S, T, CNOT} forces arbitrary angles to be approximated via gridsynth.
circuit = QuantumCircuit(2)
circuit.add_H_gate(0)
circuit.add_RX_gate(0, 0.5)
circuit.add_RY_gate(1, 0.7)
circuit.add_CNOT_gate(0, 1)
circuit.add_RZ_gate(1, 0.3)
target_gates = [gate_names.H, gate_names.T, gate_names.CNOT, gate_names.RZ]
transpiled = GateSetConversionTranspiler(target_gates)(circuit)
print(Counter(g.name for g in transpiled.gates))
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.
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.
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.
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)]