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.

  1. Backend (hardware) adaptation

  2. 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 SamplingBackends, 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)]