tket Beginner Free 7/8 in series 12 min read

Hello World in tket (pytket)

Your first quantum program using pytket, Quantinuum's high-performance quantum SDK with built-in circuit compilation and multi-backend support.

What you'll learn

  • tket
  • pytket
  • quantinuum
  • bell state
  • quantum circuits
  • compiler

Prerequisites

  • Basic Python (variables, functions, loops)
  • No quantum physics background needed

What is tket?

tket (pronounced “ticket”) is a quantum SDK created by Quantinuum (formerly Cambridge Quantum Computing). It is best known for its advanced circuit compiler, but tket is much more than an optimizer. It is a complete framework for building, compiling, and executing quantum circuits across many different hardware platforms.

The Python interface is called pytket, and it is the primary way most developers interact with tket.

How tket represents circuits internally

tket represents quantum circuits as directed acyclic graphs (DAGs) rather than flat gate lists. Each node in the DAG corresponds to a gate operation, and edges represent qubit wires flowing between gates. This is a fundamental architectural difference from frameworks like Qiskit, where circuits are stored as ordered lists of gate instructions.

The DAG representation gives tket a structural advantage for optimization. Because the graph encodes which gates can commute past one another, the compiler can identify optimization opportunities that a flat gate list would obscure. For example, if two gates act on non-overlapping qubits, the DAG makes it obvious that they are independent, and the compiler can freely reorder them to create cancellation opportunities.

This is why tket’s optimizer often outperforms other compilers on complex circuits. The compilation pipeline transforms the DAG through a sequence of passes, each of which rewrites the graph according to specific rules: removing redundant gates, merging rotations, rebasing to a target gate set, and routing qubits to match hardware connectivity.

Why Quantinuum builds tket

Quantinuum develops tket because it powers the compilation stack for their H-series trapped-ion quantum computers. Every circuit submitted to Quantinuum hardware passes through tket’s compiler before execution. This commercial incentive means tket receives continuous investment and optimization, particularly for trapped-ion gate sets. However, tket is open-source and supports hardware from IBM, IonQ, Rigetti, and others through its extension system.

How tket differs from other SDKs

FeaturetketQiskitCirq
Internal representationDAG with commutation analysisOrdered gate list (DAGCircuit available)Moment-based scheduling
Compiler qualityExcellent, especially for trapped-ionGood, with multiple optimization levelsGood, with custom pass support
Multi-backend supportNative via extensionsPrimarily IBM hardwarePrimarily Google hardware
Native hardware targetQuantinuum H-seriesIBM Eagle/HeronGoogle Sycamore
Circuit syntaxcircuit.H(0)qc.h(0)cirq.H(q)
Gate naming conventionUppercase (H, CX, Rz)Lowercase (h, cx, rz)cirq.H, cirq.CNOT objects
Angle conventionHalf-turns (1.0 = pi radians)Radians (pi = pi radians)Half-turns (1.0 = pi radians)

Installation

pip install pytket

For specific backends, install the relevant extension:

pip install pytket-qiskit       # IBM Quantum + Aer simulator
pip install pytket-quantinuum   # Quantinuum H-series hardware
pip install pytket-ionq         # IonQ hardware
pip install pytket-braket       # AWS Braket

Each extension package provides a backend class that handles compilation to the target hardware’s native gate set, qubit routing, and job submission.

Hello World: Bell State

from pytket import Circuit
from pytket.extensions.qiskit import AerBackend

# 1. Build the circuit
circuit = Circuit(2, 2)   # 2 qubits, 2 classical bits
circuit.H(0)              # Hadamard on qubit 0: creates superposition
circuit.CX(0, 1)          # CNOT: entangle qubits 0 and 1
circuit.measure_all()     # Measure all qubits

# 2. Choose a backend
backend = AerBackend()    # Qiskit Aer simulator via pytket extension

# 3. Compile the circuit for the backend
compiled = backend.get_compiled_circuit(circuit)

# 4. Run
handle = backend.process_circuit(compiled, n_shots=1000)
result = backend.get_result(handle)

# 5. Read results
counts = result.get_counts()
print(counts)
# Counter({(0, 0): 497, (1, 1): 503})  - only 00 and 11 appear

What the results mean

The output is a Counter object that maps outcome tuples to counts. For our Bell state, the possible outcomes are:

  • (0, 0): both qubits measured as 0
  • (1, 1): both qubits measured as 1

The two outcomes appear roughly 50/50, and the outcomes (0, 1) and (1, 0) never appear. This confirms that the qubits are entangled: measuring one qubit instantly determines the other.

The Circuit Object in Depth

tket’s Circuit class provides a rich set of inspection tools that let you examine every aspect of your circuit.

Basic circuit properties

from pytket import Circuit

circuit = Circuit(3, 3)
circuit.H(0)
circuit.CX(0, 1)
circuit.CX(1, 2)
circuit.measure_all()

# Basic metrics
print(circuit.n_gates)         # Total gate count (including measurements)
print(circuit.n_qubits)        # Number of qubits: 3
print(circuit.depth())         # Circuit depth (longest path through the DAG)
print(circuit.n_2qb_gates())   # Number of two-qubit gates: 2

Iterating over commands

Each gate in the circuit is represented as a Command object. You can iterate over all commands to inspect the circuit gate by gate:

from pytket import Circuit

circuit = Circuit(2)
circuit.H(0)
circuit.Rz(0.25, 0)
circuit.CX(0, 1)

# Get the list of all commands
commands = circuit.get_commands()
for cmd in commands:
    print(f"Gate: {cmd.op.type}, Params: {cmd.op.params}, Qubits: {cmd.qubits}")

# Output:
# Gate: OpType.H, Params: [], Qubits: [q[0]]
# Gate: OpType.Rz, Params: [0.25], Qubits: [q[0]]
# Gate: OpType.CX, Params: [], Qubits: [q[0], q[1]]

The cmd.op.type field gives you the OpType enum value, cmd.op.params gives a list of angle parameters (in half-turns), and cmd.qubits gives the list of qubits the gate acts on.

Alternative gate construction with add_gate

In addition to the shorthand methods like circuit.H(0), you can use the more explicit add_gate method:

from pytket import Circuit, OpType

circuit = Circuit(2)

# These two lines are equivalent:
circuit.H(0)
circuit.add_gate(OpType.H, [], [0])

# For parameterized gates, pass the parameter list:
circuit.Rx(0.5, 0)
circuit.add_gate(OpType.Rx, [0.5], [0])

The add_gate form is useful when you are building circuits programmatically and the gate type comes from a variable rather than a hardcoded method name.

Exporting to dictionary

For serialization or debugging, you can export the entire circuit as a dictionary:

circuit_dict = circuit.to_dict()
print(circuit_dict)
# Returns a nested dictionary with full circuit structure,
# including all gates, qubits, bits, and metadata

Gate Naming Conventions

tket uses uppercase names for all gates. This is different from Qiskit (lowercase) and Cirq (object-based). Here is a reference table of the most common gates:

Single-qubit gates

tket nameQiskit equivalentCirq equivalentDescription
circuit.H(q)qc.h(q)cirq.H(q)Hadamard
circuit.X(q)qc.x(q)cirq.X(q)Pauli-X (NOT)
circuit.Y(q)qc.y(q)cirq.Y(q)Pauli-Y
circuit.Z(q)qc.z(q)cirq.Z(q)Pauli-Z
circuit.S(q)qc.s(q)cirq.S(q)S gate (sqrt of Z)
circuit.Sdg(q)qc.sdg(q)cirq.S(q)**-1S-dagger
circuit.T(q)qc.t(q)cirq.T(q)T gate (fourth root of Z)
circuit.Tdg(q)qc.tdg(q)cirq.T(q)**-1T-dagger
circuit.Rx(a, q)qc.rx(a*pi, q)cirq.rx(a*pi)(q)X-rotation
circuit.Ry(a, q)qc.ry(a*pi, q)cirq.ry(a*pi)(q)Y-rotation
circuit.Rz(a, q)qc.rz(a*pi, q)cirq.rz(a*pi)(q)Z-rotation

Two-qubit gates

tket nameQiskit equivalentCirq equivalentDescription
circuit.CX(c, t)qc.cx(c, t)cirq.CNOT(c, t)Controlled-X (CNOT)
circuit.CZ(q0, q1)qc.cz(q0, q1)cirq.CZ(q0, q1)Controlled-Z
circuit.SWAP(q0, q1)qc.swap(q0, q1)cirq.SWAP(q0, q1)SWAP
circuit.ZZPhase(a, q0, q1)N/A (manual)cirq.ZZPowGate(exponent=a)ZZ interaction

Three-qubit gates

tket nameQiskit equivalentDescription
circuit.CCX(c0, c1, t)qc.ccx(c0, c1, t)Toffoli (controlled-controlled-X)

Angle convention: half-turns, not radians

This is one of the most important things to understand about tket. All parameterized gates in tket use half-turns, where 1.0 corresponds to pi radians. This means:

from pytket import Circuit

circuit = Circuit(1)

# tket: Rz(0.5) applies a rotation of 0.5 * pi = pi/2 radians
circuit.Rz(0.5, 0)

# The Qiskit equivalent would be: qc.rz(pi/2, 0)
# The relationship is: tket_angle = qiskit_angle / pi

Here are some common angle values:

tket half-turnsRadiansDegreesCommon name
0.25pi/445T gate equivalent
0.5pi/290S gate equivalent
1.0pi180Z gate equivalent
2.02*pi360Identity

So circuit.Rz(0.25, 0) is equivalent to applying a T gate, and circuit.Rz(1.0, 0) is equivalent to a Z gate.

Measurement Flexibility

tket provides several ways to add measurements to your circuit, depending on how much control you need.

Pattern 1: measure_all()

The simplest approach measures every qubit into a corresponding classical bit:

from pytket import Circuit

circuit = Circuit(3, 3)
circuit.H(0)
circuit.CX(0, 1)
circuit.CX(0, 2)
circuit.measure_all()  # Measures q[0]->c[0], q[1]->c[1], q[2]->c[2]

Pattern 2: Selective measurement with Measure

You can measure specific qubits to specific classical bits:

from pytket import Circuit

circuit = Circuit(3, 2)   # 3 qubits, only 2 classical bits
circuit.H(0)
circuit.CX(0, 1)
circuit.CX(0, 2)

# Only measure qubits 0 and 2
circuit.Measure(0, 0)    # Measure qubit 0 into classical bit 0
circuit.Measure(2, 1)    # Measure qubit 2 into classical bit 1

Pattern 3: Separate quantum and classical registers

When building circuits with different numbers of qubits and classical bits, you specify both sizes in the constructor:

from pytket import Circuit

# 4 qubits but only 2 classical bits (for partial measurement)
circuit = Circuit(4, 2)
circuit.H(0)
circuit.CX(0, 1)
circuit.CX(0, 2)
circuit.CX(0, 3)

# Only measure the first and last qubit
circuit.Measure(0, 0)
circuit.Measure(3, 1)

You can check how many classical bits a circuit has with circuit.n_bits. This is useful when you want to verify that a circuit already has measurements before adding more.

Working with Results

The result object returned by backend.get_result(handle) provides several ways to access your data.

Counts: the most common output

from pytket import Circuit
from pytket.extensions.qiskit import AerBackend

circuit = Circuit(2, 2)
circuit.H(0)
circuit.CX(0, 1)
circuit.measure_all()

backend = AerBackend()
compiled = backend.get_compiled_circuit(circuit)
handle = backend.process_circuit(compiled, n_shots=1000)
result = backend.get_result(handle)

# Returns a Counter with tuple keys
counts = result.get_counts()
print(counts)
# Counter({(0, 0): 502, (1, 1): 498})

Note that pytket returns Counter objects with tuple keys, not string keys. Each tuple element corresponds to a classical bit in order.

Converting tuple keys to bitstrings

If you want readable bitstring keys (for example, to match Qiskit’s output format), convert them:

# Convert tuple keys to bitstring keys
bitstring_counts = {}
for outcome_tuple, count in counts.items():
    # Convert tuple (0, 1) to string "01"
    bitstring = ''.join(str(b) for b in outcome_tuple)
    bitstring_counts[bitstring] = count

print(bitstring_counts)
# {'00': 502, '11': 498}

Shots: raw measurement data

For statistical analysis, you can retrieve the raw shot data as a 2D NumPy array:

import numpy as np

shots = result.get_shots()
print(shots.shape)    # (1000, 2) for 1000 shots on 2 qubits
print(shots[:5])
# [[0 0]
#  [1 1]
#  [0 0]
#  [1 1]
#  [0 0]]

Each row is one shot, and each column is one classical bit. This format is convenient for computing correlations or feeding into classical post-processing.

Statevector: for simulation

When using a statevector backend (no measurements in the circuit), you get the full quantum state:

from pytket import Circuit
from pytket.extensions.qiskit import AerStateBackend

circuit = Circuit(2)
circuit.H(0)
circuit.CX(0, 1)
# No measurements for statevector simulation

backend = AerStateBackend()
compiled = backend.get_compiled_circuit(circuit)
handle = backend.process_circuit(compiled)
result = backend.get_result(handle)

sv = result.get_state()
print(sv)
# [0.707+0j, 0+0j, 0+0j, 0.707+0j]
# This is (|00> + |11>) / sqrt(2), confirming the Bell state

The statevector is a NumPy array of complex amplitudes. For n qubits, the array has 2^n entries.

Three-Qubit GHZ State

The GHZ state extends the Bell state to three or more qubits. All qubits are maximally entangled, producing only the outcomes where every qubit agrees:

from pytket import Circuit
from pytket.extensions.qiskit import AerBackend

circuit = Circuit(3, 3)
circuit.H(0)         # Superposition on q0
circuit.CX(0, 1)     # Entangle q0 with q1
circuit.CX(0, 2)     # Entangle q0 with q2
circuit.measure_all()

backend = AerBackend()
compiled = backend.get_compiled_circuit(circuit)
handle = backend.process_circuit(compiled, n_shots=1000)
result = backend.get_result(handle)

counts = result.get_counts()
print(counts)
# Counter({(0, 0, 0): ~500, (1, 1, 1): ~500})

Only the (0, 0, 0) and (1, 1, 1) outcomes appear. No partial-agreement outcomes like (0, 1, 0) are ever observed.

The tket Compiler: Circuit Optimisation

This is where tket’s DAG-based architecture pays off. The compiler rewrites circuits to use fewer gates without changing the unitary operation the circuit performs.

Optimization with detailed metrics

Let us build a deliberately inefficient 4-qubit circuit and watch the compiler clean it up:

from pytket import Circuit
from pytket.passes import FullPeepholeOptimise

# Build a circuit with many redundant gates
circuit = Circuit(4)
circuit.H(0)
circuit.H(0)       # H*H = Identity (redundant pair)
circuit.CX(0, 1)
circuit.CX(0, 1)   # CX*CX = Identity (redundant pair)
circuit.Z(2)
circuit.Z(2)       # Z*Z = Identity (redundant pair)
circuit.T(3)
circuit.Tdg(3)     # T*Tdg = Identity (redundant pair)
circuit.H(1)
circuit.CX(1, 2)
circuit.Rz(0.3, 2)
circuit.CX(2, 3)
circuit.Ry(0.7, 3)

# Record metrics before optimization
print("=== Before Optimisation ===")
print(f"Total gates:     {circuit.n_gates}")
print(f"Two-qubit gates: {circuit.n_2qb_gates()}")
print(f"Circuit depth:   {circuit.depth()}")

# Apply full peephole optimization
FullPeepholeOptimise().apply(circuit)

# Record metrics after optimization
print("\n=== After Optimisation ===")
print(f"Total gates:     {circuit.n_gates}")
print(f"Two-qubit gates: {circuit.n_2qb_gates()}")
print(f"Circuit depth:   {circuit.depth()}")

The compiler identifies and removes all four redundant gate pairs, then further optimizes the remaining gates by merging adjacent single-qubit rotations.

Step-by-step optimization with individual passes

Instead of applying FullPeepholeOptimise all at once, you can apply individual passes and observe the effect of each one:

from pytket import Circuit
from pytket.passes import CommuteThroughMultis, RemoveRedundancies

# Build the same redundant circuit
circuit = Circuit(4)
circuit.H(0)
circuit.H(0)
circuit.CX(0, 1)
circuit.CX(0, 1)
circuit.Z(2)
circuit.Z(2)
circuit.T(3)
circuit.Tdg(3)
circuit.H(1)
circuit.CX(1, 2)
circuit.Rz(0.3, 2)
circuit.CX(2, 3)
circuit.Ry(0.7, 3)

print(f"Original:                  {circuit.n_gates} gates, depth {circuit.depth()}")

# Step 1: Commute single-qubit gates through multi-qubit gates
# This rearranges gates to create more cancellation opportunities
CommuteThroughMultis().apply(circuit)
print(f"After CommuteThroughMultis: {circuit.n_gates} gates, depth {circuit.depth()}")

# Step 2: Remove redundant gate pairs (H*H, CX*CX, Z*Z, T*Tdg)
RemoveRedundancies().apply(circuit)
print(f"After RemoveRedundancies:  {circuit.n_gates} gates, depth {circuit.depth()}")

This two-step process shows how the compiler pipeline works: first rearrange gates to expose cancellation opportunities, then remove the redundant pairs.

RebaseIBM: Compiling to IBM Native Gates

IBM quantum hardware natively supports only a small set of gates: {Rz, SX, X, CX}. Any circuit you want to run on IBM hardware must be expressed using only these gates. The RebaseIBM pass handles this conversion automatically.

from pytket import Circuit
from pytket.passes import RebaseIBM

# Build a simple circuit with non-native gates
circuit = Circuit(2)
circuit.H(0)
circuit.T(0)
circuit.CX(0, 1)

print("=== Before RebaseIBM ===")
for cmd in circuit.get_commands():
    print(f"  {cmd.op.type}: params={cmd.op.params}, qubits={cmd.qubits}")

# Rebase to IBM's native gate set
RebaseIBM().apply(circuit)

print("\n=== After RebaseIBM ===")
for cmd in circuit.get_commands():
    print(f"  {cmd.op.type}: params={cmd.op.params}, qubits={cmd.qubits}")

Here is what happens to specific gates during rebasing:

Hadamard (H) decomposes into:

  • Rz(0.5) then SX then Rz(0.5)
  • This works because H = Rz(pi/2) * SX * Rz(pi/2) up to global phase

T gate decomposes into:

  • Rz(0.25)
  • The T gate is simply a Z-rotation by pi/4, which is Rz(0.25) in half-turns

CX is already in the IBM native gate set, so it passes through unchanged.

RebaseCustom for other gate sets

If you target hardware with a different native gate set, you can define a custom rebase. For most common targets, tket provides built-in rebase passes. The key concept is that any single-qubit unitary can be decomposed into a sequence of rotations from the target set, and any two-qubit unitary can be built from the target’s two-qubit gate plus single-qubit corrections.

PauliSimp for Clifford+T Circuits

The PauliSimp pass is designed for circuits composed primarily of Clifford gates (H, S, CX, X, Y, Z) and T gates. It works by analyzing the circuit’s structure in terms of Pauli operators and grouping them into Pauli gadgets, which are tensor products of Pauli matrices exponentiated by an angle. This representation often reveals simplifications that are invisible at the gate level.

PauliSimp is particularly effective for circuits that arise in variational quantum algorithms (VQE, QAOA), where you frequently see patterns like CX-Rz-CX that implement Pauli exponentials.

from pytket import Circuit
from pytket.passes import PauliSimp

# Build a VQE-style circuit with Pauli exponentials
# exp(i * theta * Z tensor Z) is implemented as CX - Rz - CX
circuit = Circuit(4)

# First layer: single-qubit rotations
circuit.Ry(0.3, 0)
circuit.Ry(0.5, 1)
circuit.Ry(0.7, 2)
circuit.Ry(0.2, 3)

# ZZ interaction between qubits 0 and 1
circuit.CX(0, 1)
circuit.Rz(0.4, 1)
circuit.CX(0, 1)

# ZZ interaction between qubits 1 and 2
circuit.CX(1, 2)
circuit.Rz(0.3, 2)
circuit.CX(1, 2)

# ZZ interaction between qubits 2 and 3
circuit.CX(2, 3)
circuit.Rz(0.5, 3)
circuit.CX(2, 3)

# Second layer of single-qubit rotations
circuit.Ry(0.1, 0)
circuit.Ry(0.6, 1)
circuit.Ry(0.4, 2)
circuit.Ry(0.8, 3)

print(f"Before PauliSimp: {circuit.n_gates} gates, {circuit.n_2qb_gates()} two-qubit gates")

PauliSimp().apply(circuit)

print(f"After PauliSimp:  {circuit.n_gates} gates, {circuit.n_2qb_gates()} two-qubit gates")

PauliSimp recognizes the CX-Rz-CX patterns as Pauli exponentials, represents them in a compact form, and resynthesizes the circuit with fewer gates.

Backend Compilation Levels

When you call backend.get_compiled_circuit(), you can specify an optimisation level that controls how aggressively the compiler transforms your circuit:

  • Level 0: No optimization. Only applies the minimum transformations needed to make the circuit compatible with the backend (gate rebase, qubit routing).
  • Level 1: Light optimization. Applies basic simplifications like removing redundancies, in addition to the level 0 transformations. This is the default.
  • Level 2: Full optimization. Applies aggressive optimization including peephole passes and Pauli simplification, on top of everything in levels 0 and 1.
from pytket import Circuit
from pytket.extensions.qiskit import AerBackend

# Build a circuit with some redundancy
circuit = Circuit(4, 4)
circuit.H(0)
circuit.H(0)          # Redundant pair
circuit.CX(0, 1)
circuit.CX(0, 1)      # Redundant pair
circuit.H(1)
circuit.CX(1, 2)
circuit.CX(2, 3)
circuit.Rz(0.5, 3)
circuit.CX(2, 3)
circuit.CX(1, 2)
circuit.H(1)
circuit.measure_all()

backend = AerBackend()

# Compare all three levels
for level in [0, 1, 2]:
    compiled = backend.get_compiled_circuit(circuit, optimisation_level=level)
    print(f"Level {level}: {compiled.n_gates} gates, "
          f"{compiled.n_2qb_gates()} two-qubit, depth {compiled.depth()}")

For a simple Bell state, all three levels may produce similar output. The differences become pronounced on larger circuits with redundancy, where level 2 can significantly reduce gate count and circuit depth.

Multi-Backend Support

One of tket’s strongest features is that the same circuit runs on any supported backend with no code changes beyond swapping the backend object. tket handles all hardware-specific gate decompositions, qubit mapping, and routing internally.

from pytket import Circuit
from pytket.extensions.qiskit import AerBackend
# from pytket.extensions.quantinuum import QuantinuumBackend
# from pytket.extensions.ionq import IonQBackend
# from pytket.extensions.braket import BraketBackend

circuit = Circuit(2, 2)
circuit.H(0)
circuit.CX(0, 1)
circuit.measure_all()

# Swap this line to target different hardware:
backend = AerBackend()
# backend = QuantinuumBackend("H1-1E")  # Quantinuum emulator
# backend = IonQBackend()               # IonQ hardware

compiled = backend.get_compiled_circuit(circuit)
handle = backend.process_circuit(compiled, n_shots=1000)
result = backend.get_result(handle)
print(result.get_counts())

Backend comparison table

Backend classHardware vendorNative gate setConnectivity1Q error (typical)2Q error (typical)Access model
AerBackendSimulation (IBM Aer)UniversalAll-to-allNone (ideal)None (ideal)Free, local
QuantinuumBackend H1-1Quantinuum (trapped ion)Rz, PhasedX, ZZPhaseAll-to-all~0.01%~0.2%Azure Quantum / direct
IonQBackend AriaIonQ (trapped ion)GPI, GPI2, MSAll-to-all~0.03%~0.4%AWS Braket / Azure / direct
IBMQBackend (pytket-qiskit)IBM (superconducting)Rz, SX, X, CX/ECRHeavy-hex topology~0.02%~0.5-1%IBM Quantum
BraketBackend IonQIonQ via AWS BraketGPI, GPI2, MSAll-to-all~0.03%~0.4%AWS Braket

Error rates are approximate and vary across devices and calibration cycles. Trapped-ion systems offer all-to-all connectivity (no SWAP routing needed), while superconducting systems require qubit routing to match their fixed topology.

Running on Quantinuum Hardware

Quantinuum’s H-series quantum computers are among the highest-fidelity quantum processors available. They use trapped-ion qubits, which provide several unique advantages:

  • All-to-all connectivity: Any qubit can interact directly with any other qubit. This eliminates the SWAP gates that superconducting processors need to route operations between non-adjacent qubits.
  • Very low error rates: Two-qubit gate fidelities exceed 99.8% (error rate ~0.2%), which is among the best in the industry.
  • Mid-circuit measurement: You can measure individual qubits in the middle of a computation and use the classical result to conditionally apply later gates. This is essential for quantum error correction and certain algorithms.
  • Real-time classical feedback: Classical logic can execute between quantum operations, enabling adaptive circuits and feed-forward protocols.

Emulator vs. hardware

Quantinuum provides a noise-aware emulator for each hardware device. The emulator mimics the noise characteristics of the real hardware, so you can test your circuits before spending hardware credits:

  • "H1-1E": Emulator for the H1-1 device (free to use, runs in cloud)
  • "H1-1": Actual H1-1 hardware (requires credits/subscription)
from pytket.extensions.quantinuum import QuantinuumBackend

# Use the emulator for testing
backend = QuantinuumBackend("H1-1E")
backend.login()

circuit = Circuit(2, 2)
circuit.H(0)
circuit.CX(0, 1)
circuit.measure_all()

# Level 2 optimization is recommended for hardware to minimize gate count
compiled = backend.get_compiled_circuit(circuit, optimisation_level=2)
handle = backend.process_circuit(compiled, n_shots=100)
result = backend.get_result(handle)
print(result.get_counts())

Quantinuum native gate set

When tket compiles for Quantinuum hardware, it targets the native gate set:

  • Rz(angle): Z-axis rotation
  • PhasedX(angle, phase): A single-qubit gate that generalizes Rx and Ry rotations
  • ZZPhase(angle): A two-qubit gate that implements exp(-i * angle * pi/2 * Z tensor Z)

After compilation with optimisation_level=2, you can inspect the compiled circuit to verify it only contains these native gates:

compiled = backend.get_compiled_circuit(circuit, optimisation_level=2)
for cmd in compiled.get_commands():
    print(cmd.op.type, cmd.op.params, cmd.qubits)

Common Mistakes and Pitfalls

Here are five specific mistakes that new tket users frequently encounter.

1. Forgetting to compile before running

Every backend expects circuits in a specific format (correct gate set, valid qubit routing, etc.). If you skip the compilation step, the backend will either reject the circuit or silently produce incorrect results.

# WRONG: submitting an uncompiled circuit
handle = backend.process_circuit(circuit, n_shots=1000)  # May fail!

# CORRECT: always compile first
compiled = backend.get_compiled_circuit(circuit)
handle = backend.process_circuit(compiled, n_shots=1000)

Always call backend.get_compiled_circuit(circuit) before backend.process_circuit().

2. Confusing tket angle units with Qiskit angle units

tket uses half-turns (1.0 = pi radians), while Qiskit uses radians (pi = pi radians). This means the same numerical value produces different rotations in each framework.

# tket: Rz(0.5) rotates by 0.5 * pi = pi/2 radians
circuit.Rz(0.5, 0)

# Qiskit equivalent: qc.rz(pi/2, 0)  -- NOT qc.rz(0.5, 0)!

If you are porting a circuit from Qiskit to tket, divide all angles by pi. If you are porting from tket to Qiskit, multiply all angles by pi.

3. Using measure_all() on a circuit that already has measurements

Calling circuit.measure_all() adds a measurement from every qubit to a classical bit. If the circuit already has measurements (or if you call it twice), you end up with duplicate measurements that can cause errors or unexpected behavior.

# Check before adding measurements
if circuit.n_bits == 0:
    # Safe to add measurements
    circuit = Circuit(2, 2)
    circuit.H(0)
    circuit.CX(0, 1)
    circuit.measure_all()
else:
    print("Circuit already has classical bits; check for existing measurements")

4. Comparing counts with string keys instead of tuple keys

pytket returns Counter objects with tuple keys like (0, 1), not string keys like "01". Code that expects string keys will break silently.

counts = result.get_counts()

# WRONG: this key will never match
if "00" in counts:
    print(counts["00"])

# CORRECT: use tuple keys
if (0, 0) in counts:
    print(counts[(0, 0)])

# Or convert to string keys explicitly
str_counts = {''.join(str(b) for b in k): v for k, v in counts.items()}
if "00" in str_counts:
    print(str_counts["00"])

5. Applying FullPeepholeOptimise after rebasing to hardware gates

FullPeepholeOptimise works with tket’s internal TK1 gate representation. If you have already rebased your circuit to a hardware-specific gate set (like IBM’s Rz, SX, X, CX), the optimizer may reintroduce TK1 gates during its analysis. You then need to rebase again afterward.

from pytket import Circuit
from pytket.passes import FullPeepholeOptimise, RebaseIBM

circuit = Circuit(2)
circuit.H(0)
circuit.T(0)
circuit.CX(0, 1)
circuit.H(1)

# RECOMMENDED ORDER: optimize first, then rebase
FullPeepholeOptimise().apply(circuit)
RebaseIBM().apply(circuit)

# AVOID: rebasing first then optimizing (optimizer may undo the rebase)
# RebaseIBM().apply(circuit)
# FullPeepholeOptimise().apply(circuit)  # May reintroduce TK1 gates!
# RebaseIBM().apply(circuit)             # Need to rebase again

The cleanest workflow is to optimize first and rebase last. If you do need to optimize after rebasing, apply the rebase pass again as the final step.

Next Steps

The pytket documentation covers the full API in detail, including advanced compilation passes, custom routing algorithms, and noise-aware compilation. To explore IBM’s latest hardware through tket, install pytket-qiskit. To test the Quantinuum emulator, sign up for an account at Quantinuum’s TKET Cloud portal and use the "H1-1E" backend string. For optimization benchmarks, try building progressively more complex circuits and comparing gate counts before and after FullPeepholeOptimise at different optimisation levels.

Was this tutorial helpful?