PennyLane Advanced Free 5/26 in series 60 minutes

Decomposing Arbitrary Gates into Clifford+T with PennyLane

Understand why Clifford+T is the universal fault-tolerant gate set, the cost of T gates in magic state distillation, and how to analyze T-count with PennyLane.

What you'll learn

  • Clifford+T
  • gate decomposition
  • fault tolerance
  • T count
  • PennyLane
  • Solovay-Kitaev

Prerequisites

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

Why Clifford+T?

Fault-tolerant quantum computing does not run arbitrary unitaries directly. Instead, error-corrected hardware natively implements a restricted set of gates that can be executed transversally, meaning that a logical gate can be applied bitwise across physical qubits without spreading errors. The most widely studied such set is Clifford+T:

  • Clifford gates: H (Hadamard), S (phase), CNOT. These can be implemented transversally in many error-correcting codes including the surface code.
  • T gate: the pi/8 gate, defined as T = diag(1, e^{i*pi/4}). This gate cannot be implemented transversally in the surface code, and requires a costly procedure called magic state distillation.

Together, Clifford gates and the T gate form a universal gate set. Any unitary can be approximated to any desired precision using only these gates. The key insight driving fault-tolerant algorithm design is that the cost of a T gate is orders of magnitude higher than a Clifford gate, so T-count (the number of T gates in a decomposition) is the primary resource metric.

The Cost of the T Gate: Magic State Distillation

To implement a T gate fault-tolerantly, you need a high-fidelity magic state |A> = T|+> = (|0> + e^{i*pi/4}|1>) / sqrt(2). Creating such a state from noisy physical operations requires a distillation factory: a large ancilla circuit that takes many noisy copies of |A> and outputs fewer, higher-fidelity copies.

The overhead is significant. Depending on the target logical error rate and physical error rate, a single T gate may consume:

  • 15 to 500+ physical qubits (for the distillation factory)
  • 10x to 1000x more time than a Clifford gate

This is why T-count optimization is an active research area. Reducing T-count by even a small factor can dramatically shrink the physical qubit and time overhead of a fault-tolerant algorithm.

Setup

pip install pennylane pennylane-qchem numpy
import pennylane as qml
from pennylane import numpy as np
import pennylane.ops as ops

Clifford+T as a Gate Set in PennyLane

PennyLane natively supports Clifford gates and the T gate. Let us verify the set:

# Clifford generators
H  = qml.Hadamard(wires=0)
S  = qml.S(wires=0)
CNOT = qml.CNOT(wires=[0, 1])

# The T gate
T  = qml.T(wires=0)
Td = qml.adjoint(qml.T)(wires=0)  # T-dagger

# Verify T^2 = S
import numpy as np
T_matrix  = qml.matrix(qml.T(wires=0))
S_matrix  = qml.matrix(qml.S(wires=0))
T2_matrix = T_matrix @ T_matrix
print("T^2 == S:", np.allclose(T2_matrix, S_matrix))

Decomposing Gates with PennyLane’s Decompose

PennyLane’s decompose() function recursively expands gates into simpler operations. Combined with custom stopping criteria, you can control how deep the decomposition goes.

from pennylane.transforms import decompose

dev = qml.device("default.qubit", wires=2)

@qml.qnode(dev)
def circuit():
    qml.T(wires=0)
    qml.S(wires=0)
    qml.Hadamard(wires=0)
    return qml.state()

# Decompose until only Clifford+T remains
def is_clifford_t(op):
    return op.name in {"Hadamard", "S", "Adjoint(S)", "T", "Adjoint(T)", "CNOT", "PauliX", "PauliY", "PauliZ", "Identity"}

tape = qml.tape.QuantumScript.from_queue(qml.queuing.AnnotatedQueue())
with qml.queuing.AnnotatedQueue() as q:
    circuit.construct([], {})

A more practical approach is to inspect decompositions for specific rotation gates:

def decompose_to_clifford_t(gate):
    """Recursively decompose a gate, printing each level."""
    try:
        decomp = gate.decomposition()
        print(f"{gate.name} decomposes to: {[g.name for g in decomp]}")
        return decomp
    except Exception as e:
        print(f"{gate.name}: no further decomposition ({e})")
        return [gate]

# Rz(pi/4) = T
print("Rz(pi/4):")
decompose_to_clifford_t(qml.RZ(np.pi / 4, wires=0))

# Rz(pi/2) = S
print("\nRz(pi/2):")
decompose_to_clifford_t(qml.RZ(np.pi / 2, wires=0))

# General Rz
print("\nRz(pi/3):")
decompose_to_clifford_t(qml.RZ(np.pi / 3, wires=0))

The Solovay-Kitaev Theorem

The Solovay-Kitaev theorem is the theoretical guarantee that Clifford+T is efficiently universal. It states that any single-qubit unitary U can be approximated to precision epsilon using O(log^c(1/epsilon)) gates from a dense, inverse-closed gate set, where c is approximately 3.97 in the original proof (later improved to ~1.93).

In practice, this means:

  • Approximating U to precision 0.01 requires roughly 60 to 100 Clifford+T gates.
  • Approximating U to precision 0.001 requires roughly 200 to 400 Clifford+T gates.
  • Each additional digit of precision multiplies the T-count by roughly 3 to 10.

The SK algorithm works by:

  1. Finding a coarse approximation using a precomputed lookup table.
  2. Recursively using group commutator sequences to tighten the approximation.
  3. Each recursion level approximately squares the precision.

PennyLane does not currently ship a full SK compiler, but understanding the theorem tells you what to expect from any Clifford+T synthesis tool.

T-Count for Common Single-Qubit Rotations

Here is a reference table for Rz rotations expressed as fractions of pi:

RotationExact Clifford+T?Minimum T-count
Rz(pi/4)Yes (= T)1
Rz(pi/2)Yes (= S)0 (Clifford)
Rz(pi)Yes (= Z)0 (Clifford)
Rz(pi/8)Yes3
Rz(pi/16)Yes7
Rz(pi/3)No (irrational)~10 for 10^-3
Rz(0.1)No (irrational)~50 for 10^-3

The pattern for dyadic fractions is clear: Rz(pi/2^k) requires 2^(k-1) - 1 T gates. Arbitrary angles require approximation via SK or other synthesis methods and the T-count grows logarithmically with required precision.

Analyzing T-Count in a PennyLane Circuit

You can count T gates in a circuit tape:

def count_t_gates(circuit_fn, *args, **kwargs):
    """Count T and T-dagger gates in a PennyLane circuit."""
    with qml.tape.QuantumTape() as tape:
        circuit_fn(*args, **kwargs)
    
    t_names = {"T", "Adjoint(T)"}
    t_count = sum(1 for op in tape.operations if op.name in t_names)
    clifford_names = {"Hadamard", "S", "Adjoint(S)", "CNOT", "PauliX", "PauliY", "PauliZ"}
    c_count = sum(1 for op in tape.operations if op.name in clifford_names)
    
    return {"T_count": t_count, "Clifford_count": c_count, "total": len(tape.operations)}

def my_circuit():
    qml.T(wires=0)
    qml.Hadamard(wires=0)
    qml.T(wires=0)
    qml.adjoint(qml.T)(wires=0)
    qml.CNOT(wires=[0, 1])
    qml.T(wires=1)

counts = count_t_gates(my_circuit)
print(f"T-count:      {counts['T_count']}")
print(f"Clifford count: {counts['Clifford_count']}")

T-Count Optimization Strategies

Several strategies reduce T-count beyond what naive synthesis produces:

1. Phase polynomials. For circuits with only CNOT and T gates, represent the computation as a phase polynomial and use algebraic simplification to cancel T gates. The t-par algorithm achieves optimal or near-optimal T-count for this class.

2. ZX-calculus. Represent the circuit as a ZX-diagram and apply graph rewrite rules. Tools like PyZX can systematically reduce T-count using spider fusion, pivot, and local complementation rules.

3. Rotation merging. When two Rz rotations are applied to the same qubit with only Clifford gates in between, they can sometimes be merged into a single rotation with smaller angle, reducing the T-count of the combined approximation.

4. Repeat-until-success (RUS) circuits. For specific rotations, probabilistic circuits can achieve the same logical effect with lower expected T-count than deterministic approaches.

A Complete Clifford+T Circuit Example

Here is a circuit that implements a Toffoli gate using only Clifford+T gates (T-count = 7, which is known to be optimal):

@qml.qnode(qml.device("default.qubit", wires=3))
def toffoli_clifford_t():
    # Control qubits: 0, 1. Target qubit: 2.
    # Optimal Clifford+T Toffoli decomposition (T-count = 7)
    qml.Hadamard(wires=2)
    qml.CNOT(wires=[1, 2])
    qml.adjoint(qml.T)(wires=2)
    qml.CNOT(wires=[0, 2])
    qml.T(wires=2)
    qml.CNOT(wires=[1, 2])
    qml.adjoint(qml.T)(wires=2)
    qml.CNOT(wires=[0, 2])
    qml.T(wires=1)
    qml.T(wires=2)
    qml.CNOT(wires=[0, 1])
    qml.Hadamard(wires=2)
    qml.T(wires=0)
    qml.adjoint(qml.T)(wires=1)
    qml.CNOT(wires=[0, 1])
    return qml.state()

state = toffoli_clifford_t()
print("Toffoli via Clifford+T succeeded. State vector has", len(state), "components.")

Verify correctness by comparing to qml.Toffoli:

@qml.qnode(qml.device("default.qubit", wires=3))
def toffoli_native():
    qml.Toffoli(wires=[0, 1, 2])
    return qml.state()

native_matrix = qml.matrix(toffoli_native)()
ct_matrix     = qml.matrix(toffoli_clifford_t)()
print("Matrices match:", np.allclose(native_matrix, ct_matrix))

Key Takeaways

  • Clifford gates are cheap in fault-tolerant computing; T gates are expensive due to magic state distillation.
  • The Solovay-Kitaev theorem guarantees efficient approximation of any unitary in O(log^c(1/epsilon)) gates.
  • T-count is the primary cost metric for fault-tolerant algorithm design.
  • PennyLane’s decompose() and tape inspection tools let you analyze and count T gates in any circuit.
  • Optimization techniques including phase polynomials, ZX-calculus, and rotation merging can significantly reduce T-count beyond naive synthesis.

Was this tutorial helpful?