PennyLane
Install
Install
  1. Compilation/
  2. Pauli Frame Tracking

Pauli Frame Tracking

OverviewDetailsQuantum Error CorrectionResources

Pauli frame tracking is a technique related to both compilation and quantum error correction that (i) speeds up quantum programs by decoupling error decoding from Clifford gate execution, (ii) removes the need to apply certain gates on hardware, and (iii) thus reduces the potential noise introduced to the quantum computer. In exchange, a classical computer must perform additional up-front compilation and real-time bookkeeping alongside the quantum computer.

In Pauli frame tracking, each logical qubit, q, in a quantum program has an associated Pauli record, R_q, that is one member of the set \{I, X, Z, XZ\}. For storage, only 2 classical bits are required per logical qubit. Thus, for N logical qubits, 2N classical bits are needed. Pauli records compactly store the effect of all Pauli and Clifford gates that have been applied to the associated qubits. Examples of non-Pauli Clifford gates include \{H, S, CNOT\} while examples of non-Clifford gates include \{T, \mathrm{Toffoli}, CCZ\}.

A software implementation consists of 5 steps for each Pauli record:

  1. Initialise the qubit to be |0\rangle and its Pauli record to be I
  2. Upon measurement, correct the measurement based on the Pauli record
    • If the final Pauli record is X or XZ, bitflip the measurement result
    • Otherwise, do not modify the measurement result
  3. When a Pauli gate must be applied, update the Pauli record but do not physically apply the gate
  4. When a Clifford gate must be applied, update the Pauli record and physically apply the gate
  5. When a non-Clifford gate must be applied, physically apply the Pauli record and reset to I [called flushing]. Then, apply the non-Clifford gate.

Inputs

  • A quantum program consisting of Pauli, Clifford, and non-Clifford gates.

Outputs

  • A quantum program consisting of Clifford and non-Clifford gates, and potentially zero Pauli gates.
  • A Pauli record alongside each logical qubit.

Example

Worked example of Pauli frame tracking

Figure 1: Worked example of Pauli frame tracking. The top circuit represents the original circuit consisting of Pauli gates, Clifford gates, and a non-Clifford gate. The middle row represents the classical Pauli record for each qubit that is updated after every gate, and the bottom circuit is the processed circuit that will be executed on hardware. Notice that the Pauli gates in the original circuit have been removed in the circuit that will be executed on hardware.

PauliClifford
XY=iZ HZ=XH
YX=-iZSX=YS
XZ=-iYCNOT (X\otimes I) = (X\otimes X) CNOT
ZX=iY
YZ=iX
ZY=-iX

Table 1: Relevant conjugation rules that are exploited in Pauli frame tracking.

This example demonstrates that fewer Pauli gates need to be physically executed and that measurement results can be corrected with the Pauli record. Note that we are not considering any quantum error correction in this two-qubit example.

import pennylane as qml
from pennylane.tape import QuantumTape, QuantumScript
from pennylane import numpy as np   

num_qubits = 2

dev = qml.device('default.qubit', wires=num_qubits)

@qml.qnode(dev)
def circuit():
    ## Pauli gates
    qml.X(0)
    qml.Y(0)
    qml.Barrier()

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

    ## Non-Clifford gate
    qml.T(0)
    qml.Barrier()
    # return qml.sample(wires=[0,1])
    return qml.state()

circuit.construct([],{})

print("This is the original circuit")
qml.drawer.use_style('pennylane_sketch')
fig, ax = qml.draw_mpl(circuit)()
Original circuit

We can see that the original circuit, above, consists of some Pauli gates, followed by Clifford gates, and a non-Clifford gate (the T gate). The barriers in the diagram visually separate these gate types.

Below, we shall initiate the Pauli record in binary symplectic form, allowing for more efficient modifications to the record. We also define a method to decode a given Pauli record entry. Both represent a part of the classical compilation workload.

## Initialise Pauli records for all qubits to I
# I: [0,0], X: [1,0], Z: [0,1], Y ~ XZ: [1,1]
Pauli_record = np.zeros((num_qubits, 2), dtype=int) 

# Create a tape from the original circuit
tape = qml.workflow.construct_tape(circuit)()

## This method translates from the symplectic form to the name of the Pauli gate. 
def decode(a):
    if np.array_equal(a, np.array([0, 0])):
        return "I"
    elif np.array_equal(a, np.array([0, 1])):
        return "Z"
    elif np.array_equal(a, np.array([1, 0])):
        return "X"
    elif np.array_equal(a, np.array([1, 1])):
        return "Y"
    else:
        raise ValueError("Invalid input")

Tapes are how PennyLane stores the sequence of gates to be applied in a circuit. Now, we present both the real-time and the upfront classical compilation workload. Upfront classical work updates the Pauli record and removes Pauli gates from the circuit to be physically executed. Real-time classical code is run alongside the quantum program execution mainly for when non-Clifford gates must be executed.


# Iterate through the operations in the tape and print their details
# Modify the Pauli record based on the gates applied
# Also, construct a new circuit based on the Pauli record
with qml.tape.QuantumTape() as new_tape: 
    for op in tape.operations:
        if op.name == 'Barrier':
            qml.Barrier()
            continue  # Skip barriers

        ## Pauli gates (never apply these!)
        elif op.name == 'PauliX':
            Pauli_record[op.wires[0]] ^= [1,0]
        elif op.name == 'PauliY':
            Pauli_record[op.wires[0]] ^= [1,1]
        elif op.name == 'PauliZ':
            Pauli_record[op.wires[0]] ^= [0,1]

        ## Clifford gates
        elif op.name == 'Hadamard':
            Pauli_record[op.wires[0]] = Pauli_record[op.wires[0]][::-1]  # Swap X and Z
            qml.Hadamard(op.wires[0])  
        elif op.name == 'CNOT':
            control, target = op.wires
            Pauli_record[control, 1] ^= Pauli_record[control,1]^Pauli_record[target,1]
            Pauli_record[target, 0] ^= Pauli_record[control,0]^Pauli_record[target,0]
            qml.CNOT(wires=[control, target])
        elif op.name == 'S':
            Pauli_record[op.wires[0], 1] = Pauli_record[op.wires[0], 0] ^ Pauli_record[op.wires[0], 1]  
            qml.S(op.wires[0])
        ## Non-Clifford one-qubit gate
        elif op.name == 'T':
            # Apply the Pauli record to the current qubit
            gate = decode(Pauli_record[op.wires[0]])
            if gate == "X":
                qml.X(op.wires[0])
            elif gate == "Z":
                qml.Z(op.wires[0])
            elif gate == "Y":
                qml.Y(op.wires[0])
            
            qml.T(op.wires[0])
            # Flush the Pauli record
            Pauli_record[op.wires[0]]=[0,0]

        print(f"Gate: {op.name}")
        print(f"Wires: {op.wires.tolist()}")
        print(f"Parameters: {op.data}")
        print(Pauli_record)
        print("---")

    ## As before, return the state
    qml.state()

## Draw the circuit after Pauli frame tracking
qml.drawer.use_style('pennylane_sketch')
fig, ax = qml.drawer.tape_mpl(new_tape)
New circuit

The Pauli gates from the initial circuit are no longer present. However, because the Pauli record was X for the first qubit prior to the execution of the T gate, there is an X gate before the T gate in the circuit to be physically executed. Now, we consider the final state of this new circuit.

print("Original circuit result: ")
print(circuit())

print("New circuit result: ")
qml.execute([new_tape], dev)
Original circuit result: 
array([0. -0.70710678j, 0. +0.j, 0. +0.j, 0.5+0.5j])
New circuit result: 
(array([0. +0.j, 0. +0.70710678j, 0.5+0.5j, 0. +0.j]),)

The final state of this new circuit is not the same as the original circuit because a bitflip correction may need to occur, based on the final Pauli record. For this example, the second qubit's measurements must be bitflipped.

Typical Usage

  • When quantum error correction is used, especially when the time to decode an error is significantly longer than the time to execute a gate
  • When Pauli-based computation is required to prepare a quantum program for quantum error correction encoding
  • When a quantum program must be optimised for circuit depth and/or runtime

References

[1] "Fault-tolerant quantum computing in the Pauli or Clifford frame with slow error diagnostics", Chamberland, C. et al., arXiv:1704.06662v2, 2017.

[2] "Effective Fault-Tolerant Quantum Computation with Slow Measurements", DiVincenzo, D.P. and Aliferis, P., arXiv:quant-ph/0607047, 2006.

[3] "Pauli Frames for Quantum Computer Architectures", Riesebos, L. et al., https://dl.acm.org/doi/epdf/10.1145/3061639.3062300, 2017.

Cite this page

@misc{PennyLane-PauliFrameTracking,
    title = "Pauli Frame Tracking",
    author = "David D.W. Ren",
    year = "2026",
    howpublished = "\url{https://pennylane.ai/compilation/pauli-frame-tracking}"
}

Page author(s)

David Ren
David Ren

David Ren

David is a quantum scientist at Xanadu.

PennyLane

PennyLane is a cross-platform Python library for quantum computing, quantum machine learning, and quantum chemistry. Built by researchers, for research. Created with ❤️ by Xanadu.

Research

  • Research
  • Performance
  • Hardware & Simulators
  • Demos
  • Quantum Compilation
  • Quantum Datasets

Education

  • Teach
  • Learn
  • Codebook
  • Coding Challenges
  • Videos
  • Glossary

Software

  • Install PennyLane
  • Features
  • Documentation
  • Catalyst Compilation Docs
  • Development Guide
  • API
  • GitHub
Stay updated with our newsletter

© Copyright 2026 | Xanadu | All rights reserved

TensorFlow, the TensorFlow logo and any related marks are trademarks of Google Inc.

Privacy Policy|Terms of Service|Cookie Policy|Code of Conduct