tket Advanced Free 6/8 in series 60 minutes

Advanced Circuit Optimization with tket

Master advanced tket compilation: custom PassManagers, PauliSimp, CliffordSimp, noise-aware routing with Architecture graphs, KAK decomposition, and backend-agnostic portability across IBM, IonQ, and Quantinuum.

What you'll learn

  • tket
  • Pytket
  • circuit optimization
  • noise-aware compilation
  • backend agnostic

Prerequisites

  • Strong Python skills
  • Solid quantum computing foundations
  • Linear algebra and complex numbers

Why Advanced Compilation Matters

Raw circuit depth is the enemy of quantum computation on NISQ hardware. Every additional two-qubit gate accumulates error. A well-compiled circuit for a 20-qubit VQE might have 40% fewer CNOT gates than a naively assembled one, the difference between a meaningful result and noise.

tket (from Quantinuum) is a backend-agnostic compilation framework built around a composable pass system. Unlike hardware-vendor SDKs that optimize only for their own devices, tket exports to IBM, IonQ, Quantinuum, AWS Braket, Rigetti, and others from a single circuit object.

This tutorial assumes you have pytket installed:

pip install pytket
pip install pytket-qiskit pytket-quantinuum pytket-ionq  # backend extensions

The Pass System

tket’s compiler is a pipeline of BasePass objects. Each pass transforms the circuit in place. Passes compose with SequencePass (run in order) and RepeatUntilSatisfiedPass (run until a predicate holds).

from pytket.passes import (
    SequencePass,
    CliffordSimp,
    PauliSimp,
    FullPeepholeOptimise,
    DecomposeBoxes,
    SynthesiseTK2,
)
from pytket.circuit import Circuit

circ = Circuit(4)
circ.H(0).CX(0, 1).CX(1, 2).CX(2, 3)
circ.Rz(0.25, 0).CX(0, 1).Rz(-0.25, 1).CX(0, 1)

# Build a custom optimization pipeline
my_pass = SequencePass([
    DecomposeBoxes(),
    CliffordSimp(),
    PauliSimp(),
    FullPeepholeOptimise(),
])

my_pass.apply(circ)
print(f"Two-qubit gate count: {circ.n_2qb_gates()}")

CliffordSimp: Exploiting Clifford Structure

Clifford gates (H, S, CX, CZ, SWAP) form a group with well-understood algebra. CliffordSimp recognizes patterns of Clifford gates and replaces them with shorter equivalent sequences by propagating Pauli operators through the circuit using the tableau representation.

from pytket.circuit import Circuit
from pytket.passes import CliffordSimp

circ = Circuit(3)
# Redundant Clifford structure
circ.H(0).CX(0, 1).H(0).H(0).CX(0, 1).H(0)
circ.CX(1, 2).CX(2, 1).CX(1, 2)   # SWAP via 3 CNOTs

before = circ.n_gates
CliffordSimp().apply(circ)
after = circ.n_gates

print(f"Gates before: {before}, after: {after}")

CliffordSimp can reduce circuits dominated by Clifford gates dramatically, sometimes eliminating entire blocks that cancel to identity.

PauliSimp: Optimizing Pauli Exponentials

Many quantum chemistry and QAOA circuits are built from Pauli exponential terms: exp(-i * theta/2 * P) where P is a tensor product of Pauli operators. PauliSimp converts the circuit to a sequence of these exponentials, recombines commuting terms, and then re-synthesizes into a minimal CNOT ladder.

from pytket.circuit import Circuit
from pytket.passes import PauliSimp
from pytket.pauli import Pauli, QubitPauliString
from pytket.utils import QubitPauliOperator

circ = Circuit(4)
# Simulate a chemistry-inspired circuit
circ.H(0)
circ.CX(0, 1).Rz(0.3, 1).CX(0, 1)
circ.CX(1, 2).Rz(0.5, 2).CX(1, 2)
circ.CX(2, 3).Rz(0.7, 3).CX(2, 3)
circ.H(0)

before_2q = circ.n_2qb_gates()
PauliSimp().apply(circ)
after_2q = circ.n_2qb_gates()

print(f"Two-qubit gates before: {before_2q}, after: {after_2q}")

PauliSimp is especially powerful for variational ansatze that use UCCSD (unitary coupled cluster) excitation operators, often halving or better the two-qubit gate count.

KAK Decomposition vs SynthesiseIBM

The KAK decomposition is the canonical way to decompose an arbitrary two-qubit unitary into at most 3 CNOT gates plus single-qubit rotations. tket’s SynthesiseTK2 pass applies KAK to decompose arbitrary two-qubit unitaries into TK2 gates (tket’s native parameterized two-qubit gate), then a backend-specific pass converts TK2 into hardware-native gates.

SynthesiseIBM is a specialized pass that decomposes circuits specifically into IBM’s {CX, U3} basis, using KAK internally but then applying IBM-specific peephole optimizations.

from pytket.circuit import Circuit, OpType
from pytket.passes import SynthesiseTK2, SynthesiseIBM, FullPeepholeOptimise

def count_2q(circ):
    return circ.n_2qb_gates()

# Generic KAK via TK2
circ1 = Circuit(2)
circ1.Unitary2qBox([[1,0,0,0],[0,0,1,0],[0,1,0,0],[0,0,0,1]], 0, 1)

from pytket.passes import DecomposeBoxes
DecomposeBoxes().apply(circ1)
SynthesiseTK2().apply(circ1)
print(f"TK2 path, 2q gates: {count_2q(circ1)}")

circ2 = Circuit(2)
circ2.Unitary2qBox([[1,0,0,0],[0,0,1,0],[0,1,0,0],[0,0,0,1]], 0, 1)
DecomposeBoxes().apply(circ2)
SynthesiseIBM().apply(circ2)
print(f"IBM path, 2q gates: {count_2q(circ2)}")

For circuits targeting IBM backends, SynthesiseIBM followed by FullPeepholeOptimise generally gives the best results. For other backends, start with SynthesiseTK2 and then apply the backend-specific rebase pass.

Routing with Architecture Graphs

Real devices only allow two-qubit gates between physically connected qubit pairs. tket’s routing system inserts SWAP gates to move qubit state to adjacent positions. You describe the topology as an Architecture object.

from pytket.architecture import Architecture
from pytket.passes import DefaultMappingPass, RoutingPass, PlacementPass
from pytket.placement import GraphPlacement, NoiseAwarePlacement
from pytket.circuit import Circuit

# Define a linear 5-qubit chain topology: 0-1-2-3-4
edges = [(0, 1), (1, 2), (2, 3), (3, 4)]
arch = Architecture(edges)

circ = Circuit(5)
circ.CX(0, 2)   # not directly connected -- needs routing
circ.CX(1, 4)   # also needs routing
circ.CX(0, 4)

# GraphPlacement minimizes SWAP overhead by graph matching
GraphPlacement(arch).place(circ)
RoutingPass(arch).apply(circ)

print(f"Circuit after routing: {circ.n_2qb_gates()} two-qubit gates")

The GraphPlacement pass solves a subgraph isomorphism to find the best initial mapping of logical to physical qubits before routing begins. A good initial placement dramatically reduces the number of SWAPs inserted.

Noise-Aware Placement

NoiseAwarePlacement goes further by using device calibration data (gate error rates and readout errors) to place qubits on the highest-fidelity physical locations.

from pytket.placement import NoiseAwarePlacement
from pytket.backends.backendinfo import BackendInfo
from pytket.architecture import Architecture

# In practice you get BackendInfo from a backend object, e.g.:
# from pytket.extensions.qiskit import IBMQBackend
# backend = IBMQBackend("ibm_brisbane")
# backend_info = backend.backend_info

# For illustration, build a minimal BackendInfo manually
arch = Architecture([(0,1),(1,2),(2,3),(3,4)])

# With a real backend:
# NoiseAwarePlacement(arch, backend_info.gate_errors,
#                    backend_info.readout_errors,
#                    backend_info.link_errors).place(circ)

# Noise-aware placement chooses physical qubits with lowest error rates
# and maps entangling operations to the highest-fidelity links

On a real IBM backend the difference between naive placement and noise-aware placement can be 10-30% in circuit fidelity for medium-depth circuits.

Custom PassManager Pipelines

For production workloads, assemble a full pipeline tailored to your target backend:

from pytket.passes import (
    SequencePass,
    DecomposeBoxes,
    PauliSimp,
    CliffordSimp,
    FullPeepholeOptimise,
    RebaseIBM,
    SynthesiseIBM,
)
from pytket.architecture import Architecture
from pytket.passes import DefaultMappingPass
from pytket.placement import NoiseAwarePlacement

def compile_for_ibm(circ, arch, backend_info=None):
    pipeline = SequencePass([
        DecomposeBoxes(),
        PauliSimp(),
        CliffordSimp(),
        SynthesiseIBM(),
        FullPeepholeOptimise(),
        DefaultMappingPass(arch),  # handles placement + routing
        RebaseIBM(),
        FullPeepholeOptimise(),    # clean up after routing
    ])
    pipeline.apply(circ)
    return circ

The double FullPeepholeOptimise (once before routing to reduce gates, once after to clean up SWAPs) is a common pattern that pays off in lower final gate counts.

Backend-Agnostic Portability

The same optimized Circuit object can be dispatched to different backends by applying the appropriate rebase pass at the end:

from pytket.passes import RebaseIBM, RebaseQuantinuum, RebaseIonQ
from pytket.circuit import Circuit

circ = Circuit(3)
circ.H(0).CX(0,1).CX(1,2).Rz(0.5, 2)

# For IBM (CX + U3 basis)
circ_ibm = circ.copy()
RebaseIBM().apply(circ_ibm)

# For Quantinuum (ZZPhase + TK1 basis)
circ_h = circ.copy()
RebaseQuantinuum().apply(circ_h)

# For IonQ (XX + GPI basis)
circ_ionq = circ.copy()
RebaseIonQ().apply(circ_ionq)

print("IBM gates:", circ_ibm.n_gates)
print("Quantinuum gates:", circ_h.n_gates)
print("IonQ gates:", circ_ionq.n_gates)

This backend-agnostic workflow is one of tket’s most valuable properties for teams that run on multiple hardware platforms or benchmark across providers.

Comparing Optimization Passes

A practical comparison for a 6-qubit QAOA circuit:

Pass Combination2Q Gate CountDepth
No optimization4862
CliffordSimp only3851
PauliSimp only2940
PauliSimp + CliffordSimp2435
Full pipeline (above)1928

Numbers are illustrative and circuit-dependent, but the trend is consistent: combining Pauli-level and Clifford-level optimization before routing, then cleaning up after routing, beats any single pass by a significant margin.

Next Steps

  • Use pytket-quantinuum to submit to H-series trapped-ion hardware, which natively supports ZZPhase gates for efficient Hamiltonian simulation.
  • Explore pytket-cirq and pytket-braket for AWS and Google backends.
  • For error mitigation on top of compiled circuits, combine tket with Mitiq; tket handles compilation, Mitiq wraps execution with zero-noise extrapolation or probabilistic error cancellation.

Was this tutorial helpful?