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:
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 };
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
patchtype 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 measurementsThis 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.
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_roundare the flattened X and Z stabilizer schedule matrices, which can encode an optimized gate order on top of the parity-check support. Seecudaq::qec::code::get_stabilizer_schedule_x()for the encoding and the default (the plain parity matrices).Protected Members:
operation_encodings: Maps operations to their quantum kernel implementations. The key is theoperationenum 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:
Create a New Class:
class my_code : public qec::code { protected: // Implement required virtual methods public: my_code(const heterogeneous_map& options); };
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;
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_roundkernel are the flattened X and Z stabilizer schedule matrices returned bycudaq::qec::code::get_stabilizer_schedule_x()andcudaq::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 theget_stabilizer_schedule_*methods to encode a gate order, in which case entryk >= 1means the interaction executes at timestepk(the built-insurface_codedoes this to avoid hook errors). A kernel that only needs the support pattern should therefore test entries for!= 0rather than== 1.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 calleddistance, you can retrieve that from the user viamy_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); }
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:
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)
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" }); }
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.
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
Define Quantum Kernels:
Implement the required quantum kernels using the
@cudaq.kerneldecorator:@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_roundmust be annotated to returnlist[cudaq.measure_handle].Note
As in C++, the two list arguments passed to the
stabilizer_roundkernel are the flattened X and Z stabilizer schedule matrices, which default to the parity-check matrices. A Python code can optionally defineget_stabilizer_schedule_x/get_stabilizer_schedule_zmethods returning a 2D array with the same shape and support pattern as the corresponding parity-check matrix, where entryk >= 1schedules that interaction at timestepk.Implement the Code Class:
Create a class decorated with
@qec.codethat 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
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.codedecorator takes the name of the code as an argumentOperation encodings are registered via the
operation_encodingsdictionaryStabilizer generators are defined as a list of
cudaq.SpinOperatorThe 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
patchtype 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):
XIIXIIIIIIXXIXXIIIIIIXXIXXIIIIIIXIIX
Z-type (weight 2 on the top and bottom boundaries, weight 4 in the bulk):
IZZIIIIIIZZIZZIIIIIIIIZZIZZIIIIIIZZI
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_grid — C++:
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();