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 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
| Feature | tket | Qiskit | Cirq |
|---|---|---|---|
| Internal representation | DAG with commutation analysis | Ordered gate list (DAGCircuit available) | Moment-based scheduling |
| Compiler quality | Excellent, especially for trapped-ion | Good, with multiple optimization levels | Good, with custom pass support |
| Multi-backend support | Native via extensions | Primarily IBM hardware | Primarily Google hardware |
| Native hardware target | Quantinuum H-series | IBM Eagle/Heron | Google Sycamore |
| Circuit syntax | circuit.H(0) | qc.h(0) | cirq.H(q) |
| Gate naming convention | Uppercase (H, CX, Rz) | Lowercase (h, cx, rz) | cirq.H, cirq.CNOT objects |
| Angle convention | Half-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 name | Qiskit equivalent | Cirq equivalent | Description |
|---|---|---|---|
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)**-1 | S-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)**-1 | T-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 name | Qiskit equivalent | Cirq equivalent | Description |
|---|---|---|---|
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 name | Qiskit equivalent | Description |
|---|---|---|
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-turns | Radians | Degrees | Common name |
|---|---|---|---|
| 0.25 | pi/4 | 45 | T gate equivalent |
| 0.5 | pi/2 | 90 | S gate equivalent |
| 1.0 | pi | 180 | Z gate equivalent |
| 2.0 | 2*pi | 360 | Identity |
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)thenSXthenRz(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 class | Hardware vendor | Native gate set | Connectivity | 1Q error (typical) | 2Q error (typical) | Access model |
|---|---|---|---|---|---|---|
AerBackend | Simulation (IBM Aer) | Universal | All-to-all | None (ideal) | None (ideal) | Free, local |
QuantinuumBackend H1-1 | Quantinuum (trapped ion) | Rz, PhasedX, ZZPhase | All-to-all | ~0.01% | ~0.2% | Azure Quantum / direct |
IonQBackend Aria | IonQ (trapped ion) | GPI, GPI2, MS | All-to-all | ~0.03% | ~0.4% | AWS Braket / Azure / direct |
IBMQBackend (pytket-qiskit) | IBM (superconducting) | Rz, SX, X, CX/ECR | Heavy-hex topology | ~0.02% | ~0.5-1% | IBM Quantum |
BraketBackend IonQ | IonQ via AWS Braket | GPI, GPI2, MS | All-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?