QEC Codes

The cudaq-qec code interface (cudaq::qec::code) defines what a quantum error correcting code is: a mapping from logical operations to their physical CUDA-Q kernel implementations. This page covers the framework — the class structure and how to define or extend a code — together with the codes that ship with the library. Read it to understand the model or to implement your own code. For a runnable, end-to-end program, see the Creating New QEC Codes example.

QEC Code Framework cudaq::qec::code

The cudaq::qec::code class serves as the base class for all quantum error correcting codes in CUDA-Q QEC. It provides a flexible extension point for implementing new codes and defines the core interface that all QEC codes must support.

The core abstraction here is that of a mapping or dictionary of logical operations to their corresponding physical implementation in the error correcting code as CUDA-Q quantum kernels.

Class Structure

The code base class provides:

  1. Operation Enumeration: Defines supported logical operations

    enum class operation {
        x,     // Logical X gate
        y,     // Logical Y gate
        z,     // Logical Z gate
        h,     // Logical Hadamard gate
        s,     // Logical S gate
        cx,    // Logical CNOT gate
        cy,    // Logical CY gate
        cz,    // Logical CZ gate
        stabilizer_round,  // Stabilizer measurement round
        prep0, // Prepare |0⟩ state
        prep1, // Prepare |1⟩ state
        prepp, // Prepare |+⟩ state
        prepm  // Prepare |-⟩ state
    };
    
  2. Patch Type: Defines the structure of a logical qubit patch

    struct patch {
        cudaq::qview<> data;  // View of data qubits
        cudaq::qview<> ancx;  // View of X stabilizer ancilla qubits
        cudaq::qview<> ancz;  // View of Z stabilizer ancilla qubits
    };
    

    The patch type represents a logical qubit in quantum error correction codes. It contains: - data: A view of the data qubits in the patch - ancx: A view of the ancilla qubits used for X stabilizer measurements - ancz: A view of the ancilla qubits used for Z stabilizer measurements

    This structure is designed for use within CUDA-Q kernel code and provides a convenient way to access different qubit subsets within a logical qubit patch.

  3. Kernel Type Aliases: Defines quantum kernel signatures

    using one_qubit_encoding = cudaq::qkernel<void(patch)>;
    using two_qubit_encoding = cudaq::qkernel<void(patch, patch)>;
    using stabilizer_round = cudaq::qkernel<std::vector<cudaq::measure_result>(
        patch, const std::vector<std::size_t>&, const std::vector<std::size_t>&)>;
    

    The two vector arguments of stabilizer_round are the flattened X and Z stabilizer schedule matrices, which can encode an optimized gate order on top of the parity-check support. See cudaq::qec::code::get_stabilizer_schedule_x() for the encoding and the default (the plain parity matrices).

  4. Protected Members:

    • operation_encodings: Maps operations to their quantum kernel implementations. The key is the operation enum and the value is a variant on the above kernel type aliases.

    • m_stabilizers: Stores the code’s stabilizer generators

Implementing a New Code

To implement a new quantum error correcting code:

  1. Create a New Class:

    class my_code : public qec::code {
    protected:
        // Implement required virtual methods
    public:
        my_code(const heterogeneous_map& options);
    };
    
  2. Implement Required Virtual Methods:

    // Number of physical data qubits
    std::size_t get_num_data_qubits() const override;
    
    // Total number of ancilla qubits
    std::size_t get_num_ancilla_qubits() const override;
    
    // Number of X-type ancilla qubits
    std::size_t get_num_ancilla_x_qubits() const override;
    
    // Number of Z-type ancilla qubits
    std::size_t get_num_ancilla_z_qubits() const override;
    
  3. Define Quantum Kernels:

    Create CUDA-Q kernels for each logical operation:

    __qpu__ void x(patch p) {
        // Implement logical X
    }
    
    __qpu__ std::vector<cudaq::measure_result> stabilizer(patch p,
        const std::vector<std::size_t>& x_stabs,
        const std::vector<std::size_t>& z_stabs) {
        // Implement stabilizer measurements
    }
    

    Note

    The two vector arguments passed to the stabilizer_round kernel are the flattened X and Z stabilizer schedule matrices returned by cudaq::qec::code::get_stabilizer_schedule_x() and cudaq::qec::code::get_stabilizer_schedule_z(). By default these equal the plain parity-check matrices (every entry 0 or 1), but a code can override the get_stabilizer_schedule_* methods to encode a gate order, in which case entry k >= 1 means the interaction executes at timestep k (the built-in surface_code does this to avoid hook errors). A kernel that only needs the support pattern should therefore test entries for != 0 rather than == 1.

  4. Register Operations:

    In the constructor, register quantum kernels for each operation:

    my_code::my_code(const heterogeneous_map& options) : code() {
        // Register operations
        operation_encodings.insert(
           std::make_pair(operation::x, x));
        operation_encodings.insert(
           std::make_pair(operation::stabilizer_round, stabilizer));
    
        // Define stabilizer generators
        m_stabilizers = fromPauliWords({"XXXX", "ZZZZ"});
    }
    

    Note that in your constructor, you have access to user-provided options. For example, if your code depends on an integer parameter called distance, you can retrieve that from the user via

    my_code::my_code(const heterogeneous_map& options) : code() {
        // ... fill the map and stabilizers ...
    
        // Get the user-provided distance, or just
        // set to 3 if user did not provide one
        this->distance = options.get<int>("distance", /*defaultValue*/ 3);
    }
    
  5. Register Extension Point:

    Add extension point registration.

    class my_code : public qec::code {
        // ... members from above ...
    
        CUDAQ_EXTENSION_CUSTOM_CREATOR_FUNCTION(
            my_code,
            static std::unique_ptr<qec::code> create(
                const heterogeneous_map &options) {
                return std::make_unique<my_code>(options);
            })
    };
    
    CUDAQ_EXT_PT_REGISTER_TYPE(my_code)
    

Example: Steane Code

The Steane [[7,1,3]] code provides a complete example implementation:

  1. Header Definition:

    • Declares quantum kernels for all logical operations

    • Defines the code class with required virtual methods

    • Specifies 7 data qubits and 6 ancilla qubits (3 X-type, 3 Z-type)

  2. Implementation:

    steane::steane(const heterogeneous_map &options) : code() {
        // Register all logical operations
        operation_encodings.insert(
            std::make_pair(operation::x, x));
        // ... register other operations ...
    
        // Define stabilizer generators
        m_stabilizers = fromPauliWords({
            "XXXXIII", "IXXIXXI", "IIXXIXX",
            "ZZZZIII", "IZZIZZI", "IIZZIZZ"
        });
    }
    
  3. Quantum Kernels:

    Implements fault-tolerant logical operations:

    __qpu__ void x(patch logicalQubit) {
        // Apply logical X to specific data qubits
        x(logicalQubit.data[4], logicalQubit.data[5],
          logicalQubit.data[6]);
    }
    
    __qpu__ std::vector<cudaq::measure_result> stabilizer(patch logicalQubit,
        const std::vector<std::size_t>& x_stabilizers,
        const std::vector<std::size_t>& z_stabilizers) {
        // Measure X stabilizers
        h(logicalQubit.ancx);
        // ... apply controlled-X gates ...
        h(logicalQubit.ancx);
    
        // Measure Z stabilizers
        // ... apply controlled-X gates ...
    
        // Return measurement results
        return mz(logicalQubit.ancz, logicalQubit.ancx);
    }
    

Implementing a New Code in Python

CUDA-Q QEC supports implementing quantum error correction codes in Python using the @qec.code decorator. This provides a more accessible way to prototype and develop new codes.

  1. Create a New Python File:

    Create a new file (e.g., my_steane.py) with your code implementation:

    import cudaq
    import cudaq_qec as qec
    from cudaq_qec import patch
    
  2. Define Quantum Kernels:

    Implement the required quantum kernels using the @cudaq.kernel decorator:

    @cudaq.kernel
    def prep0(logicalQubit: patch):
        h(logicalQubit.data[0], logicalQubit.data[4], logicalQubit.data[6])
        x.ctrl(logicalQubit.data[0], logicalQubit.data[1])
        x.ctrl(logicalQubit.data[4], logicalQubit.data[5])
        # ... additional initialization gates ...
    
    
    @cudaq.kernel
    def stabilizer(logicalQubit: patch, x_stabilizers: list[int],
                   z_stabilizers: list[int]) -> list[cudaq.measure_handle]:
        # Measure X stabilizers
        h(logicalQubit.ancx)
        for xi in range(len(logicalQubit.ancx)):
            for di in range(len(logicalQubit.data)):
                if x_stabilizers[xi * len(logicalQubit.data) + di] == 1:
                    x.ctrl(logicalQubit.ancx[xi], logicalQubit.data[di])
        h(logicalQubit.ancx)
    
        # Measure Z stabilizers
        for zi in range(len(logicalQubit.ancz)):
            for di in range(len(logicalQubit.data)):
                if z_stabilizers[zi * len(logicalQubit.data) + di] == 1:
                    x.ctrl(logicalQubit.data[di], logicalQubit.ancz[zi])
    
        # Get and reset ancillas
        results = mz([*logicalQubit.ancz, *logicalQubit.ancx])
        reset(logicalQubit.ancx)
        reset(logicalQubit.ancz)
        return results
    
    
    

    Note

    The kernel registered for stabilizer_round must be annotated to return list[cudaq.measure_handle].

    Note

    As in C++, the two list arguments passed to the stabilizer_round kernel are the flattened X and Z stabilizer schedule matrices, which default to the parity-check matrices. A Python code can optionally define get_stabilizer_schedule_x / get_stabilizer_schedule_z methods returning a 2D array with the same shape and support pattern as the corresponding parity-check matrix, where entry k >= 1 schedules that interaction at timestep k.

  3. Implement the Code Class:

    Create a class decorated with @qec.code that implements the required interface:

    @qec.code('py-steane-example')
    class MySteaneCodeImpl:
    
        def __init__(self, **kwargs):
            qec.Code.__init__(self, **kwargs)
    
            # Define stabilizer generators
            stabilizers_str = [
                "XXXXIII", "IXXIXXI", "IIXXIXX", "ZZZZIII", "IZZIZZI", "IIZZIZZ"
            ]
            self.stabilizers = [
                cudaq.SpinOperator.from_word(s) for s in stabilizers_str
            ]
    
            # Define observables
            obs_str = ["IIIIXXX", "IIIIZZZ"]
            self.pauli_observables = [
                cudaq.SpinOperator.from_word(p) for p in obs_str
            ]
    
            # Register quantum kernels
            self.operation_encodings = {
                qec.operation.prep0: prep0,
                qec.operation.stabilizer_round: stabilizer
            }
    
        def get_num_data_qubits(self):
            return 7
    
        def get_num_ancilla_x_qubits(self):
            return 3
    
        def get_num_ancilla_z_qubits(self):
            return 3
    
        def get_num_ancilla_qubits(self):
            return 6
    
        def get_num_x_stabilizers(self):
            return 3
    
        def get_num_z_stabilizers(self):
            return 3
    
    
    
  4. Using the Code:

    The code can now be used like any other CUDA-Q QEC code:

    import cudaq_qec as qec
    # Either import your code directly (e.g. "import my_steane", assuming
    # your my_steane.py file is in the same directory), or you can paste the
    # above code here without importing the file.
    import my_steane
    
    # Create instance of your code
    code = qec.get_code('py-steane-example')
    
    # Use the code for various numerical experiments
    

Key Points

  • The @qec.code decorator takes the name of the code as an argument

  • Operation encodings are registered via the operation_encodings dictionary

  • Stabilizer generators are defined as a list of cudaq.SpinOperator

  • The code must implement all required methods from the base class interface

Using the Code Framework

To use an implemented code:

import cudaq_qec as qec

# Create a code instance
code = qec.get_code("steane")

# Access stabilizer information
stabilizers = code.get_stabilizers()
parity = code.get_parity()

# The code can now be used for various numerical
# experiments - see section below.
// Create a code instance
auto code = cudaq::qec::get_code("steane");

// Access stabilizer information
auto stabilizers = code->get_stabilizers();
auto parity = code->get_parity();

// The code can now be used for various numerical
// experiments - see section below.

Pre-built QEC Codes

CUDA-Q QEC provides several well-studied quantum error correction codes out of the box. Here’s a detailed overview of each:

Steane Code

The Steane code is a [[7,1,3]] CSS (Calderbank-Shor-Steane) code that encodes one logical qubit into seven physical qubits with a code distance of 3.

Key Properties:

  • Data qubits: 7

  • Encoded qubits: 1

  • Code distance: 3

  • Ancilla qubits: 6 (3 for X stabilizers, 3 for Z stabilizers)

Stabilizer Generators:

  • X-type: ["XXXXIII", "IXXIXXI", "IIXXIXX"]

  • Z-type: ["ZZZZIII", "IZZIZZI", "IIZZIZZ"]

The Steane code can correct any single-qubit error and detect up to two errors. It is particularly notable for being the smallest CSS code that can implement a universal set of transversal gates.

Usage:

import cudaq_qec as qec

# Create Steane code instance
steane = qec.get_code("steane")
auto steane = cudaq::qec::get_code("steane");

Repetition Code

The repetition code is a simple [[n,1,n]] code that protects against bit-flip (X) errors by encoding one logical qubit into n physical qubits, where n is the code distance.

Key Properties:

  • Data qubits: n (distance)

  • Encoded qubits: 1

  • Code distance: n

  • Ancilla qubits: n-1 (all for Z stabilizers)

Stabilizer Generators:

  • For distance 3: ["ZZI", "IZZ"]

  • For distance 5: ["ZZIII", "IZZII", "IIZZI", "IIIZZ"]

The repetition code is primarily educational as it can only correct X errors. However, it serves as an excellent introduction to QEC concepts.

Usage:

import cudaq_qec as qec

# Create distance-3 repetition code
code = qec.get_code('repetition', distance=3)

# Access stabilizers
stabilizers = code.get_stabilizers()  # Returns ["ZZI", "IZZ"]
auto code = qec::get_code("repetition", {{"distance", 3}});

// Access stabilizers
auto stabilizers = code->get_stabilizers();

Surface Code

The library provides a rotated surface code on a two-dimensional qubit layout with open boundaries (a single patch). It is a CSS code—\(X\) and \(Z\) errors are handled in separate CSS sectors—encoding one logical qubit into \(d^2\) data qubits with code distance \(d\) in this layout. Stabilizers have weight four in the bulk and weight two on the boundary, following the grid convention described in Towards a Standardized Definition of Quantum Circuits for Quantum Error Correction with Rotated Surface Codes.

Key Properties (distance \(d\) in this implementation):

  • Data qubits: \(d^2\)

  • Encoded logical qubits: 1

  • Code distance: \(d\)

  • Stabilizers: \(d^2 - 1\) total—\((d^2 - 1) / 2\) \(X\)-type and \((d^2 - 1) / 2\) \(Z\)-type. The patch type assigns one ancilla per stabilizer measurement, so the ancilla count matches the stabilizer count here; other hardware layouts could fold or share ancillas differently.

Stabilizer Generators (example: distance \(= 3\))

Data qubits are indexed in row-major order (left to right, top to bottom); the leftmost character of each Pauli string is qubit 0, matching the rest of this document. For \(d = 3\) there are nine data qubits:

d0  d1  d2
d3  d4  d5
d6  d7  d8
  • X-type (weight 2 on the left and right boundaries, weight 4 in the bulk):

    • XIIXIIIII

    • IXXIXXIII

    • IIIXXIXXI

    • IIIIIXIIX

  • Z-type (weight 2 on the top and bottom boundaries, weight 4 in the bulk):

    • IZZIIIIII

    • ZZIZZIIII

    • IIIIZZIZZ

    • IIIIIIZZI

These Pauli words are exactly those used internally for \(d=3\); get_stabilizers() returns the same generators in a canonical sorted order (rather than grouped as X-type then Z-type).

For other distances, stabilizer supports are generated from the same rotated grid; use stabilizer_grid or get_stabilizers() to inspect them.

You must pass distance when constructing the code; there is no default.

Orientation

The surface code accepts an optional orientation string that selects which Pauli type occupies the bulk checkerboard and which boundary pair carries the X- versus Z-type stabilizers. The first character (X or Z) sets the bulk type; the second character (H or V) sets the boundary placement. Valid values are "XV", "XH", "ZV", and "ZH" (aliases "O1", "O2", "O3", and "O4" respectively; case-insensitive). The default is "ZH", which reproduces the layout described above. The logical observables and the CNOT extraction schedule are orientation-aware, so changing the orientation changes the returned stabilizers, observables, and measurement schedule consistently.

The stabilizer_grid helper documents how stabilizers and data qubits are indexed on the grid and provides helpers to print the layout. Python: cudaq_qec.stabilizer_gridC++: cudaq::qec::surface_code::stabilizer_grid — see API. The header cudaq/qec/codes/surface_code.h contains the full declaration.

Stabilizer measurement schedule

The surface code’s stabilizer_round kernel executes one depth-4 extraction round: the X- and Z-check CNOTs are interleaved over four shared timesteps. Within each plaquette the CNOT order follows the standard zigzag schedule for the rotated surface code (Tomita & Svore): the X and Z plaquettes traverse their corners in transposed orders, selected per orientation so that mid-round ancilla faults (“hook errors”, Dennis et al.) propagate onto data-qubit pairs perpendicular to the same-type logical operator. This preserves the full code distance \(d\) under circuit-level noise; a naive schedule (both plaquette types in ascending qubit-index order) halves the effective distance of one memory basis. The schedule is available from the stabilizer_grid helper via get_cnot_schedule_x() / get_cnot_schedule_z (matrix form) and get_cnot_schedule_pairs_x / get_cnot_schedule_pairs_z (flat pair-list form).

Usage:

import cudaq_qec as qec

# Rotated surface code; distance is required
code = qec.get_code('surface_code', distance=3)  # default orientation "ZH"

# Optionally select an orientation (one of "XV", "XH", "ZV", "ZH")
code_xh = qec.get_code('surface_code', distance=3, orientation='XH')

stabilizers = code.get_stabilizers()
parity = code.get_parity()
auto code = cudaq::qec::get_code(
    "surface_code", cudaqx::heterogeneous_map{{"distance", 3}});

// Optionally select an orientation (one of "XV", "XH", "ZV", "ZH")
auto code_xh = cudaq::qec::get_code(
    "surface_code", cudaqx::heterogeneous_map{
                        {"distance", 3},
                        {"orientation", std::string("XH")}});

auto stabilizers = code->get_stabilizers();
auto parity = code->get_parity();