Skip to content

Commit df69a07

Browse files
committed
Cherry pick PR #672 (22772f7 ) back, excluding decoding-server integration
Signed-off-by: Angela Burton <angelab@nvidia.com>
1 parent e46b35f commit df69a07

17 files changed

Lines changed: 814 additions & 145 deletions

File tree

docs/sphinx/components/qec/introduction.rst

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ The code base class provides:
8383
using stabilizer_round = cudaq::qkernel<std::vector<cudaq::measure_result>(
8484
patch, const std::vector<std::size_t>&, const std::vector<std::size_t>&)>;
8585
86+
The two vector arguments of :code:`stabilizer_round` are the flattened X and
87+
Z stabilizer *schedule* matrices, which can encode an optimized gate order
88+
on top of the parity-check support. See
89+
:cpp:func:`cudaq::qec::code::get_stabilizer_schedule_x` for the encoding
90+
and the default (the plain parity matrices).
91+
8692
4. **Protected Members**:
8793

8894
- :code:`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.
@@ -136,6 +142,20 @@ To implement a new quantum error correcting code:
136142
// Implement stabilizer measurements
137143
}
138144
145+
.. note::
146+
147+
The two vector arguments passed to the :code:`stabilizer_round` kernel
148+
are the flattened X and Z stabilizer *schedule* matrices returned by
149+
:cpp:func:`cudaq::qec::code::get_stabilizer_schedule_x` and
150+
:cpp:func:`cudaq::qec::code::get_stabilizer_schedule_z`. By default
151+
these equal the plain parity-check matrices (every entry 0 or 1), but a
152+
code can override the :code:`get_stabilizer_schedule_*` methods to
153+
encode a gate order, in which case entry :code:`k >= 1` means the
154+
interaction executes at timestep :code:`k` (the built-in
155+
:code:`surface_code` does this to avoid hook errors). A kernel that only
156+
needs the support pattern should therefore test entries for
157+
:code:`!= 0` rather than :code:`== 1`.
158+
139159
4. **Register Operations**:
140160

141161
In the constructor, register quantum kernels for each operation:
@@ -267,7 +287,18 @@ to prototype and develop new codes.
267287
.. note::
268288

269289
The kernel registered for :code:`stabilizer_round` must be annotated to
270-
return :code:`list[cudaq.measure_handle]`.
290+
return :code:`list[cudaq.measure_handle]`.
291+
292+
.. note::
293+
294+
As in C++, the two list arguments passed to the
295+
:code:`stabilizer_round` kernel are the flattened X and Z stabilizer
296+
schedule matrices, which default to the parity-check matrices. A Python
297+
code can optionally define :code:`get_stabilizer_schedule_x` /
298+
:code:`get_stabilizer_schedule_z` methods returning a 2D array with the
299+
same shape and support pattern as the corresponding parity-check
300+
matrix, where entry :code:`k >= 1` schedules that interaction at
301+
timestep :code:`k`.
271302

272303
3. **Implement the Code Class**:
273304

@@ -482,6 +513,26 @@ print the layout. **Python:** :class:`cudaq_qec.stabilizer_grid` — see
482513
:ref:`qec_stabilizer_grid_cpp`. The header :file:`cudaq/qec/codes/surface_code.h`
483514
contains the full declaration.
484515

516+
**Stabilizer measurement schedule**
517+
518+
The surface code's :code:`stabilizer_round` kernel executes one depth-4
519+
extraction round: the X- and Z-check CNOTs are interleaved over four shared
520+
timesteps. Within each plaquette the CNOT order follows the standard zigzag
521+
schedule for the rotated surface code (`Tomita & Svore
522+
<https://arxiv.org/abs/1404.3747>`__): the X and Z plaquettes traverse their
523+
corners in transposed orders, selected per orientation so that mid-round
524+
ancilla faults ("hook errors", `Dennis et al.
525+
<https://arxiv.org/abs/quant-ph/0110143>`__) propagate onto data-qubit pairs
526+
perpendicular to the same-type logical operator. This preserves the full code
527+
distance :math:`d` under circuit-level noise; a naive schedule (both plaquette
528+
types in ascending qubit-index order) halves the effective distance of one
529+
memory basis. The schedule is available from the :code:`stabilizer_grid`
530+
helper via
531+
:cpp:func:`~cudaq::qec::surface_code::stabilizer_grid::get_cnot_schedule_x` /
532+
:code:`get_cnot_schedule_z` (matrix form) and
533+
:code:`get_cnot_schedule_pairs_x` / :code:`get_cnot_schedule_pairs_z` (flat
534+
pair-list form).
535+
485536
Usage:
486537

487538
.. tab:: Python

libs/qec/include/cudaq/qec/code.h

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,10 @@ class code : public cudaqx::extension_point<code, const heterogeneous_map &> {
103103
/// @brief Type alias for two qubit quantum kernels
104104
using two_qubit_encoding = cudaq::qkernel<void(patch, patch)>;
105105

106-
/// @brief Type alias for stabilizer measurement kernels
106+
/// @brief Type alias for stabilizer measurement kernels. The two vector
107+
/// arguments are the flattened X and Z stabilizer schedule matrices (see
108+
/// get_stabilizer_schedule_x/z()): entry 0 = no support, entry k >= 1 =
109+
/// interaction at timestep k.
107110
using stabilizer_round = cudaq::qkernel<std::vector<cudaq::measure_result>(
108111
patch, const std::vector<std::size_t> &,
109112
const std::vector<std::size_t> &)>;
@@ -189,6 +192,26 @@ class code : public cudaqx::extension_point<code, const heterogeneous_map &> {
189192
/// @return Tensor representing Hz
190193
cudaqx::tensor<uint8_t> get_parity_z() const;
191194

195+
/// @brief Get the X-stabilizer schedule matrix passed to the code's
196+
/// stabilizer_round kernel. Same shape and support pattern as
197+
/// get_parity_x(); entry 0 means no support, entry k >= 1 means the
198+
/// ancilla-data interaction executes at timestep k, so a code can encode an
199+
/// optimized (e.g. hook-error-aware) gate order.
200+
/// @return Tensor of scheduled interactions; the default implementation
201+
/// returns get_parity_x() unchanged (every interaction at timestep 1, i.e.
202+
/// ascending qubit-index order).
203+
virtual cudaqx::tensor<uint8_t> get_stabilizer_schedule_x() const {
204+
return get_parity_x();
205+
}
206+
207+
/// @brief Get the Z-stabilizer schedule matrix passed to the code's
208+
/// stabilizer_round kernel. See get_stabilizer_schedule_x().
209+
/// @return Tensor of scheduled interactions; the default implementation
210+
/// returns get_parity_z() unchanged.
211+
virtual cudaqx::tensor<uint8_t> get_stabilizer_schedule_z() const {
212+
return get_parity_z();
213+
}
214+
192215
/// @brief Get Lx stacked on Lz
193216
/// @return Tensor representing pauli observables
194217
cudaqx::tensor<uint8_t> get_pauli_observables_matrix() const;

libs/qec/include/cudaq/qec/codes/surface_code.h

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,41 @@ class stabilizer_grid {
196196
/// @brief Get the stabilizers as a vector of cudaq::spin_op_terms
197197
std::vector<cudaq::spin_op_term> get_spin_op_stabilizers() const;
198198

199+
/// @brief Get the CNOT schedule matrix for the X stabilizers.
200+
///
201+
/// @return Tensor with the same shape and support pattern as the X block of
202+
/// the parity check matrix (num_x_stabilizers x distance^2). Entry 0 means
203+
/// the ancilla does not touch that data qubit; entry k in [1, 4] means the
204+
/// ancilla-data CNOT executes at timestep k of the stabilizer round.
205+
///
206+
/// @note The per-plaquette CNOT order is chosen per the grid's orientation
207+
/// so that mid-round ancilla faults ("hook errors"), which propagate onto
208+
/// the data qubits of the remaining CNOTs, land perpendicular to the
209+
/// same-type logical operator instead of along it, following the standard
210+
/// zigzag schedule of https://arxiv.org/abs/1404.3747. Rows are ordered to
211+
/// match the rows of to_parity_matrix()/code::get_parity_x().
212+
cudaqx::tensor<uint8_t> get_cnot_schedule_x() const;
213+
214+
/// @brief Get the CNOT schedule matrix for the Z stabilizers.
215+
///
216+
/// @return Tensor with the same shape and support pattern as the Z block of
217+
/// the parity check matrix (num_z_stabilizers x distance^2). Entry 0 means
218+
/// the ancilla does not touch that data qubit; entry k in [1, 4] means the
219+
/// data-ancilla CNOT executes at timestep k of the stabilizer round.
220+
///
221+
/// @note See get_cnot_schedule_x() for the hook-error rationale and row
222+
/// ordering.
223+
cudaqx::tensor<uint8_t> get_cnot_schedule_z() const;
224+
225+
/// @brief Get the X-stabilizer CNOT schedule as a flat list of
226+
/// (stabilizer index, data index) pairs, ordered by timestep within each
227+
/// stabilizer — the replay format for kernels that take an explicit CNOT
228+
/// pair list. Stabilizer indices match the rows of get_cnot_schedule_x().
229+
std::vector<std::size_t> get_cnot_schedule_pairs_x() const;
230+
231+
/// @brief Z-stabilizer counterpart of get_cnot_schedule_pairs_x().
232+
std::vector<std::size_t> get_cnot_schedule_pairs_z() const;
233+
199234
/// @brief Get the observables as a vector of cudaq::spin_op_terms
200235
///
201236
/// @return The X logical observable first, followed by the Z logical
@@ -264,12 +299,15 @@ __qpu__ void prepm(patch p);
264299
///
265300
/// @brief Perform stabilizer measurements on a surface_code patch
266301
/// @param p The patch to measure
267-
/// @param x_stabilizers Indices of X stabilizers to measure
268-
/// @param z_stabilizers Indices of Z stabilizers to measure
302+
/// @param x_stabilizer_schedule Flattened X-stabilizer CNOT schedule matrix
303+
/// (see stabilizer_grid::get_cnot_schedule_x): entry 0 = no support, entry
304+
/// k >= 1 = CNOT at timestep k
305+
/// @param z_stabilizer_schedule Flattened Z-stabilizer CNOT schedule matrix
306+
/// (see stabilizer_grid::get_cnot_schedule_z)
269307
/// @return Vector of measurement results
270308
__qpu__ std::vector<cudaq::measure_result>
271-
stabilizer(patch p, const std::vector<std::size_t> &x_stabilizers,
272-
const std::vector<std::size_t> &z_stabilizers);
309+
stabilizer(patch p, const std::vector<std::size_t> &x_stabilizer_schedule,
310+
const std::vector<std::size_t> &z_stabilizer_schedule);
273311

274312
/// @brief surface_code implementation
275313
class surface_code : public cudaq::qec::code {
@@ -306,6 +344,14 @@ class surface_code : public cudaq::qec::code {
306344
surface_code(const heterogeneous_map &);
307345
// Grid constructor would be useful
308346

347+
/// @brief Get the hook-error-aware X-stabilizer CNOT schedule matrix.
348+
/// See stabilizer_grid::get_cnot_schedule_x().
349+
cudaqx::tensor<uint8_t> get_stabilizer_schedule_x() const override;
350+
351+
/// @brief Get the hook-error-aware Z-stabilizer CNOT schedule matrix.
352+
/// See stabilizer_grid::get_cnot_schedule_z().
353+
cudaqx::tensor<uint8_t> get_stabilizer_schedule_z() const override;
354+
309355
/// @brief Extension creator function for the surface_code
310356
CUDAQ_EXTENSION_CUSTOM_CREATOR_FUNCTION(
311357
surface_code, static std::unique_ptr<cudaq::qec::code> create(

libs/qec/lib/codes/surface_code.cpp

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,126 @@ stabilizer_grid::get_spin_op_stabilizers() const {
386386
return spin_op_stabs;
387387
}
388388

389+
namespace {
390+
391+
/// Corner offsets from a stabilizer grid coordinate to its four data qubits,
392+
/// listed in CNOT execution order (timesteps 1..4). Row-major ("Z"-shaped)
393+
/// visits NW, NE, SW, SE so the last two CNOTs touch a horizontally adjacent
394+
/// data pair; column-major ("N"-shaped) visits NW, SW, NE, SE so the last two
395+
/// touch a vertically adjacent pair. Assigning one shape to the X plaquettes
396+
/// and the transposed shape to the Z plaquettes is the standard zigzag
397+
/// schedule pair for the rotated surface code (Tomita & Svore,
398+
/// https://arxiv.org/abs/1404.3747; hook errors per Dennis et al.,
399+
/// https://arxiv.org/abs/quant-ph/0110143): it steers hook errors against
400+
/// the logical operators and stays conflict-free if the two timestep
401+
/// sequences are ever interleaved.
402+
constexpr int row_major_order[4][2] = {{-1, -1}, {-1, 0}, {0, -1}, {0, 0}};
403+
constexpr int col_major_order[4][2] = {{-1, -1}, {0, -1}, {-1, 0}, {0, 0}};
404+
405+
cudaqx::tensor<uint8_t>
406+
build_cnot_schedule(const std::vector<vec2d> &stab_coords,
407+
const std::map<vec2d, size_t> &data_indices,
408+
std::size_t num_data, bool use_col_major_order) {
409+
const auto &order = use_col_major_order ? col_major_order : row_major_order;
410+
std::vector<std::vector<uint8_t>> rows;
411+
rows.reserve(stab_coords.size());
412+
for (const auto &sc : stab_coords) {
413+
std::vector<uint8_t> row(num_data, 0);
414+
for (int step = 0; step < 4; ++step) {
415+
vec2d dq(sc.row + order[step][0], sc.col + order[step][1]);
416+
auto it = data_indices.find(dq);
417+
if (it != data_indices.end())
418+
row[it->second] = step + 1;
419+
}
420+
rows.push_back(std::move(row));
421+
}
422+
423+
// Match the row order of to_parity_matrix(): within one stabilizer type the
424+
// sort comparator orders rows by the index of the first supported data
425+
// qubit. That order is total only because no two same-type surface-code
426+
// stabilizers share a first-support index — enforce it rather than assume
427+
// it, since a violation would silently pair schedule rows with the wrong
428+
// ancilla/parity rows.
429+
std::vector<std::pair<std::size_t, std::size_t>>
430+
keyed; // (first support, row)
431+
keyed.reserve(rows.size());
432+
for (std::size_t r = 0; r < rows.size(); ++r) {
433+
auto it = std::find_if(rows[r].begin(), rows[r].end(),
434+
[](uint8_t v) { return v != 0; });
435+
keyed.emplace_back(it - rows[r].begin(), r);
436+
}
437+
std::sort(keyed.begin(), keyed.end());
438+
for (std::size_t r = 1; r < keyed.size(); ++r)
439+
if (keyed[r].first == keyed[r - 1].first)
440+
throw std::runtime_error(
441+
"surface-code CNOT schedule: two same-type stabilizers share their "
442+
"first supported data qubit, so the schedule rows cannot be aligned "
443+
"with the parity-matrix rows.");
444+
445+
cudaqx::tensor<uint8_t> t({rows.size(), num_data});
446+
for (std::size_t r = 0; r < rows.size(); ++r)
447+
for (std::size_t c = 0; c < num_data; ++c)
448+
t.at({r, c}) = rows[keyed[r].second][c];
449+
return t;
450+
}
451+
452+
/// Flatten a schedule matrix into (stabilizer, data) index pairs ordered by
453+
/// timestep within each stabilizer — the replay format used by kernels that
454+
/// take an explicit CNOT pair list.
455+
std::vector<std::size_t>
456+
schedule_to_pairs(const cudaqx::tensor<uint8_t> &sched) {
457+
std::vector<std::size_t> pairs;
458+
const auto num_stabs = sched.shape()[0];
459+
const auto num_data = sched.shape()[1];
460+
std::vector<std::pair<uint8_t, std::size_t>> row; // (step, data)
461+
for (std::size_t s = 0; s < num_stabs; ++s) {
462+
row.clear();
463+
for (std::size_t d = 0; d < num_data; ++d)
464+
if (sched.at({s, d}) != 0)
465+
row.emplace_back(sched.at({s, d}), d);
466+
std::sort(row.begin(), row.end());
467+
for (const auto &[step, d] : row) {
468+
pairs.push_back(s);
469+
pairs.push_back(d);
470+
}
471+
}
472+
return pairs;
473+
}
474+
475+
} // namespace
476+
477+
cudaqx::tensor<uint8_t> stabilizer_grid::get_cnot_schedule_x() const {
478+
// A fault on the ancilla mid-round propagates onto the data qubits of the
479+
// CNOTs that have not executed yet ("hook error"): an X⊗X pair on the last
480+
// two data qubits in the schedule. If the X logical runs along the top row
481+
// (horizontal), that pair must be vertical (column-major order) so hook
482+
// chains run against the logical instead of along it; otherwise the pair
483+
// must be horizontal (row-major order).
484+
const bool x_logical_on_top_row =
485+
orientation_ == sc_orientation::XV || orientation_ == sc_orientation::ZH;
486+
return build_cnot_schedule(x_stab_coords, data_indices, distance * distance,
487+
/*use_col_major_order=*/x_logical_on_top_row);
488+
}
489+
490+
cudaqx::tensor<uint8_t> stabilizer_grid::get_cnot_schedule_z() const {
491+
// Same reasoning as get_cnot_schedule_x() for Z⊗Z hook pairs versus the Z
492+
// logical, which runs perpendicular to the X logical: the Z plaquettes use
493+
// the transposed order. The two orders also form a conflict-free pair if
494+
// the X and Z timesteps are ever interleaved.
495+
const bool x_logical_on_top_row =
496+
orientation_ == sc_orientation::XV || orientation_ == sc_orientation::ZH;
497+
return build_cnot_schedule(z_stab_coords, data_indices, distance * distance,
498+
/*use_col_major_order=*/!x_logical_on_top_row);
499+
}
500+
501+
std::vector<std::size_t> stabilizer_grid::get_cnot_schedule_pairs_x() const {
502+
return schedule_to_pairs(get_cnot_schedule_x());
503+
}
504+
505+
std::vector<std::size_t> stabilizer_grid::get_cnot_schedule_pairs_z() const {
506+
return schedule_to_pairs(get_cnot_schedule_z());
507+
}
508+
389509
std::vector<cudaq::spin_op_term>
390510
stabilizer_grid::get_spin_op_observables() const {
391511
std::vector<cudaq::spin_op_term> spin_op_obs;
@@ -480,6 +600,14 @@ std::size_t surface_code::get_num_z_stabilizers() const {
480600
return (distance * distance - 1) / 2;
481601
}
482602

603+
cudaqx::tensor<uint8_t> surface_code::get_stabilizer_schedule_x() const {
604+
return grid.get_cnot_schedule_x();
605+
}
606+
607+
cudaqx::tensor<uint8_t> surface_code::get_stabilizer_schedule_z() const {
608+
return grid.get_cnot_schedule_z();
609+
}
610+
483611
/// @brief Register the surace_code type
484612
CUDAQ_EXT_PT_REGISTER_TYPE(surface_code)
485613

libs/qec/lib/codes/surface_code_device.cpp

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,26 +49,32 @@ __qpu__ void prepm(patch logicalQubit) {
4949
}
5050

5151
__qpu__ std::vector<cudaq::measure_result>
52-
stabilizer(patch logicalQubit, const std::vector<std::size_t> &x_stabilizers,
53-
const std::vector<std::size_t> &z_stabilizers) {
52+
stabilizer(patch logicalQubit,
53+
const std::vector<std::size_t> &x_stabilizer_schedule,
54+
const std::vector<std::size_t> &z_stabilizer_schedule) {
5455
for (std::size_t i = 0; i < logicalQubit.ancx.size(); i++)
5556
reset(logicalQubit.ancx[i]);
5657
for (std::size_t i = 0; i < logicalQubit.ancz.size(); i++)
5758
reset(logicalQubit.ancz[i]);
5859

60+
// The X and Z checks share the timesteps, and the surface-code schedule
61+
// always fits in one depth-4 extraction round (see
62+
// stabilizer_grid::get_cnot_schedule_x), so num_steps is a constant.
63+
const std::size_t num_steps = 4;
64+
5965
h(logicalQubit.ancx);
60-
for (std::size_t xi = 0; xi < logicalQubit.ancx.size(); ++xi)
61-
for (std::size_t di = 0; di < logicalQubit.data.size(); ++di)
62-
if (x_stabilizers[xi * logicalQubit.data.size() + di] == 1)
63-
cudaq::x<cudaq::ctrl>(logicalQubit.ancx[xi], logicalQubit.data[di]);
66+
for (std::size_t step = 1; step <= num_steps; ++step) {
67+
for (std::size_t xi = 0; xi < logicalQubit.ancx.size(); ++xi)
68+
for (std::size_t di = 0; di < logicalQubit.data.size(); ++di)
69+
if (x_stabilizer_schedule[xi * logicalQubit.data.size() + di] == step)
70+
cudaq::x<cudaq::ctrl>(logicalQubit.ancx[xi], logicalQubit.data[di]);
71+
for (std::size_t zi = 0; zi < logicalQubit.ancz.size(); ++zi)
72+
for (std::size_t di = 0; di < logicalQubit.data.size(); ++di)
73+
if (z_stabilizer_schedule[zi * logicalQubit.data.size() + di] == step)
74+
cudaq::x<cudaq::ctrl>(logicalQubit.data[di], logicalQubit.ancz[zi]);
75+
}
6476
h(logicalQubit.ancx);
6577

66-
// Now apply z_stabilizer circuit
67-
for (size_t zi = 0; zi < logicalQubit.ancz.size(); ++zi)
68-
for (size_t di = 0; di < logicalQubit.data.size(); ++di)
69-
if (z_stabilizers[zi * logicalQubit.data.size() + di] == 1)
70-
cudaq::x<cudaq::ctrl>(logicalQubit.data[di], logicalQubit.ancz[zi]);
71-
7278
// S = (S_X, S_Z), (x flip syndromes, z flip syndromes).
7379
// x flips are triggered by z-stabilizers (ancz)
7480
// z flips are triggered by x-stabilizers (ancx)

0 commit comments

Comments
 (0)