Skip to content

Commit 0859cc0

Browse files
Added bundled decoder_context struct for measurement extraction (#671)
<!-- Thanks for contributing to CUDA-Q Libraries! Please read the full Pull Request Guidelines in Contributing.md: https://github.com/NVIDIA/cudaqx/blob/main/Contributing.md#pull-request-guidelines --> ## Description In order to process enqueued syndromes, the decoder needs access to a measurement map so it knows how to form detectors. Right now, this comes from `generate_timelike_sparse_detector_matrix`, which does not include boundary detectors. Rather than refactor this function (which would require it to be aware of the stabilizer structure of the code), I include the `m2d` map calculated by `dem_from_kernel`, and bundle it into a new struct `decoder_context`, and provide a new helper function `decoder_context_from_memory_circuit` with a similar signature to `dem_from_memory_circuit`, which it essentially supersedes. This new function returns the DEM as well as the measurement map needed by the decoder. The `decoder_context` struct also provides helper methods for extracting the X/Z components, alleviating the need for helper functions (in the style of `*_dem_from_memory_circuit`) as well as allowing a single circuit analysis to provide both the X and Z decoder contexts. The PR also applies some changes to the `real_time_complete` documentation examples to use this function. New tests: `test_dem.py::test_decoder_context_from_memory_circuit_requires_noise_model` - asserts a throw on the `decoder_context_from_memory_circuit` if no noise model is provided `test_dem.py::test_decoder_context_and_components` - asserts that `decoder_context_from_memory_circuit` matches output of superceded `*_dem_from_memory_circuit` `test_dem.py::test_decoder_context_d_sparse_layout()` - asserts that `.d_sparse()` on `decoder_context` flattens m2d map into correct -1-terminated sparse vector `test_dem.py::test_decoder_context_single_type_code_empty_component` - check that empty component (i.e. x-component of a repetition code) is consistent `test_qec_stim.cpp::checkTealtimeDecodeFromMemoryCircuit` - end-to-end check of realtime API using decoder_context `test_qec_stim.cpp::checkDecoderContextAndComponents` - Verify DEM output by `decoder_context_from_memory_circuit` matches superseded `*_dem_from_memory_circuit`, and that extracting `x/z` components is idempotent and consistent. ## Runtime / performance impact N/A ## Self-review checklist Please confirm each item before requesting review. Check `[x]` or strike through and explain. ### Before requesting review - [x] I reviewed my own full diff in GitHub or my editor. - [x] PR is in Draft if it is not yet ready for review. - [x] Temporary / debugging changes have been removed. - [x] Local test logs reviewed; no unexplained warnings or errors. - [x] CI logs reviewed; no unexplained warnings or errors. - [x] Full CI has been run. ### Scope and size - [x] PR is under ~1000 lines, or an exception is justified in the description. - [x] Refactoring-only changes are isolated in their own PR(s). - [x] No existing tests were disabled or modified just to make this PR pass (if so, an issue has been raised). ### Tests - [x] New functionality has new tests. - [x] Tests fail if the new functionality is broken (including crashes), not just when it is missing. - [x] Negative tests added where exceptions are expected. - [x] Truth data added where simple `EXPECT_*` / `assert` checks are insufficient for algorithmic correctness. - [x] CI runtime impact considered; team notified if significant. ### Documentation - [x] Public-facing APIs have Doxygen docs. - [x] User-visible behavior changes have public docs, or a follow-up is tracked. ### Code style - [x] Naming follows the existing convention (`snake_case` vs `camelCase`) for the area being modified. ### Dependencies - [x] No new third-party dependencies, **or** the team has been notified and OSRB tickets filed. --------- Signed-off-by: Eliot Heinrich <eheinrich@nvidia.com> Signed-off-by: Eliot Heinrich <38039898+eliotheinrich@users.noreply.github.com>
1 parent 68299c1 commit 0859cc0

10 files changed

Lines changed: 545 additions & 101 deletions

File tree

docs/sphinx/api/qec/cpp_api.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,13 @@ Detector Error Model
3030
.. doxygenstruct:: cudaq::qec::detector_error_model
3131
:members:
3232

33+
.. doxygenstruct:: cudaq::qec::decoder_context
34+
:members:
35+
3336
.. doxygenfunction:: cudaq::qec::dem_from_memory_circuit(const code &, operation, std::size_t, cudaq::noise_model &, bool)
3437
.. doxygenfunction:: cudaq::qec::x_dem_from_memory_circuit(const code &, operation, std::size_t, cudaq::noise_model &, bool)
3538
.. doxygenfunction:: cudaq::qec::z_dem_from_memory_circuit(const code &, operation, std::size_t, cudaq::noise_model &, bool)
39+
.. doxygenfunction:: cudaq::qec::decoder_context_from_memory_circuit(const code &, operation, std::size_t, cudaq::noise_model &, bool)
3640
.. doxygenfunction:: cudaq::qec::dem_from_stim_text(const std::string &, bool)
3741

3842
Decoder Interfaces

docs/sphinx/api/qec/python_api.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,13 @@ Detector Error Model
2929
.. autoclass:: cudaq_qec.DetectorErrorModel
3030
:members:
3131

32+
.. autoclass:: cudaq_qec.DecoderContext
33+
:members:
34+
3235
.. autofunction:: cudaq_qec.dem_from_memory_circuit
3336
.. autofunction:: cudaq_qec.x_dem_from_memory_circuit
3437
.. autofunction:: cudaq_qec.z_dem_from_memory_circuit
38+
.. autofunction:: cudaq_qec.decoder_context_from_memory_circuit
3539
.. autofunction:: cudaq_qec.dem_from_stim_text
3640

3741
Decoder Interfaces

docs/sphinx/examples/qec/cpp/real_time_complete.cpp

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@
2828

2929
// [Begin Save DEM]
3030
// Save decoder configuration to YAML file
31-
void save_dem(const cudaq::qec::detector_error_model &dem,
31+
void save_dem(const cudaq::qec::decoder_inputs &inputs,
3232
const std::string &filename) {
33+
const auto &dem = inputs.dem;
3334
// Create decoder config
3435
cudaq::qec::decoding::config::decoder_config config;
3536
config.id = 0;
@@ -38,12 +39,7 @@ void save_dem(const cudaq::qec::detector_error_model &dem,
3839
config.syndrome_size = dem.num_detectors();
3940
config.H_sparse = cudaq::qec::pcm_to_sparse_vec(dem.detector_error_matrix);
4041
config.O_sparse = cudaq::qec::pcm_to_sparse_vec(dem.observables_flips_matrix);
41-
42-
// Calculate numRounds from DEM (we send 1 additional round, so add 1)
43-
uint64_t numSyndromesPerRound = 2; // Z0Z1 and Z1Z2
44-
auto numRounds = dem.num_detectors() / numSyndromesPerRound + 1;
45-
config.D_sparse = cudaq::qec::generate_timelike_sparse_detector_matrix(
46-
numSyndromesPerRound, numRounds, false);
42+
config.D_sparse = cudaq::qec::d_sparse(inputs.m2d);
4743

4844
// Decoder parameters are a plain heterogeneous_map; keys are governed by
4945
// the parameter schema the decoder registered.
@@ -120,14 +116,21 @@ __qpu__ int64_t qec_circuit() {
120116
cudaq::qec::decoding::enqueue_syndromes(0, syndromes);
121117
}
122118

119+
// Final data readout
120+
std::vector<cudaq::measure_result> data_meas = {mz(data[0]), mz(data[1]),
121+
mz(data[2])};
122+
cudaq::qec::decoding::enqueue_syndromes(0, data_meas);
123+
std::vector<bool> result = cudaq::to_bools(data_meas);
124+
123125
// Get corrections and apply them (single logical observable)
124-
auto corrections = cudaq::qec::decoding::get_corrections(0, 1);
126+
std::vector<bool> corrections = cudaq::qec::decoding::get_corrections(0, 1);
127+
result[0] = static_cast<bool>(result[0]) ^ static_cast<bool>(corrections[0]);
125128
if (corrections[0]) {
126129
for (std::size_t i = 0; i < 3; ++i)
127130
cudaq::x(data[i]);
128131
}
129132

130-
return cudaq::to_integer(cudaq::to_bools(mz(data)));
133+
return cudaq::to_integer(result);
131134
}
132135
// [End QEC Circuit]
133136

@@ -141,17 +144,17 @@ int main() {
141144
cudaq::noise_model noise;
142145
noise.add_all_qubit_channel("x", cudaq::depolarization2(0.01), 1);
143146

144-
auto dem = cudaq::qec::z_dem_from_memory_circuit(
147+
auto ctx = cudaq::qec::decoder_context_from_memory_circuit(
145148
*code, cudaq::qec::operation::prep0, 3, noise);
146149
// [End DEM Generation]
147150

148-
save_dem(dem, "config.yaml");
151+
save_dem(ctx.full_component(), "config.yaml");
149152

150153
// Step 2: Load config and run circuit
151154
printf("\nStep 2: Running circuit with decoding...\n");
152155
load_dem("config.yaml");
153156

154-
cudaq::run(10, qec_circuit);
157+
cudaq::run(10, noise, qec_circuit);
155158
printf("Ran 10 shots\n");
156159

157160
cudaq::qec::decoding::config::finalize_decoders();

docs/sphinx/examples/qec/python/real_time_complete.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,19 @@ def qec_circuit() -> int:
6363
for _ in range(3):
6464
syndromes = measure_stabilizers(logical)
6565
qec.enqueue_syndromes(0, syndromes, 0)
66+
# Final data readout
67+
data_meas = [mz(data[0]), mz(data[1]), mz(data[2])]
68+
qec.enqueue_syndromes(0, data_meas, 0)
69+
result = cudaq.to_bools(data_meas)
6670

6771
# Get corrections and apply them (single logical observable)
6872
corrections = qec.get_corrections(0, 1, False)
73+
result[0] ^= corrections[0]
6974
if corrections[0]:
7075
for i in range(3):
7176
x(data[i])
7277

73-
return cudaq.to_integer(cudaq.to_bools(mz(data)))
78+
return cudaq.to_integer(result)
7479

7580

7681
# [End QEC Circuit]
@@ -88,7 +93,9 @@ def main():
8893
noise = cudaq.NoiseModel()
8994
noise.add_all_qubit_channel("x", cudaq.Depolarization2(0.01), 1)
9095

91-
dem = qec.z_dem_from_memory_circuit(code, qec.operation.prep0, 3, noise)
96+
ctx = qec.decoder_context_from_memory_circuit(code, qec.operation.prep0, 3,
97+
noise)
98+
dem, m2d, m2o = ctx.full_component()
9299
# [End DEM Generation]
93100

94101
# [Begin Save DEM]
@@ -101,12 +108,7 @@ def main():
101108
config.H_sparse = qec.pcm_to_sparse_vec(dem.detector_error_matrix)
102109
config.O_sparse = qec.pcm_to_sparse_vec(dem.observables_flips_matrix)
103110

104-
# Calculate numRounds from DEM (we send 1 additional round, so add 1)
105-
num_syndromes_per_round = 2 # Z0Z1 and Z1Z2
106-
num_rounds = dem.detector_error_matrix.shape[
107-
0] // num_syndromes_per_round + 1
108-
config.D_sparse = qec.generate_timelike_sparse_detector_matrix(
109-
num_syndromes_per_round, num_rounds, False)
111+
config.D_sparse = qec.d_sparse(m2d)
110112
# Decoder parameters are a plain dict; keys are governed by the parameter
111113
# schema the decoder registered (see qec.decoder_param_schema).
112114
config.decoder_custom_args = {"lut_error_depth": 2}
@@ -128,7 +130,7 @@ def main():
128130
qec.configure_decoders_from_file("config.yaml")
129131
# [End Load DEM]
130132

131-
run_result = cudaq.run(qec_circuit, shots_count=10)
133+
run_result = cudaq.run(qec_circuit, shots_count=10, noise_model=noise)
132134
print("Ran 10 shots")
133135

134136
qec.finalize_decoders()

libs/qec/include/cudaq/qec/experiments.h

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
******************************************************************************/
88
#pragma once
99

10+
#include "cudaq/algorithms/dem.h"
1011
#include "cudaq/qec/code.h"
1112
#include "cudaq/qec/detector_error_model.h"
12-
#include <tuple>
13+
#include <cstddef>
1314
#include <vector>
1415

1516
namespace cudaq::qec {
@@ -159,6 +160,53 @@ std::tuple<cudaqx::tensor<uint8_t>, cudaqx::tensor<uint8_t>>
159160
sample_memory_circuit(const code &code, std::size_t numShots,
160161
std::size_t numRounds, cudaq::noise_model &noise);
161162

163+
/// @brief Finalized decoder inputs: a canonicalized DEM and measurement maps.
164+
struct decoder_inputs {
165+
detector_error_model dem;
166+
cudaq::M2DSparseMatrix m2d;
167+
cudaq::M2OSparseMatrix m2o;
168+
};
169+
170+
/// @brief Lazy handle returned by `decoder_context_from_memory_circuit`.
171+
///
172+
/// Stores the raw (uncanonicalized) circuit analysis. Call a component method
173+
/// to canonicalize exactly the stabilizer type needed and obtain a
174+
/// `decoder_inputs`:
175+
/// - `x_component()` — X-stabilizer detectors only
176+
/// - `z_component()` — Z-stabilizer detectors only
177+
/// - `full_component()` — both stabilizer types, boundary-aware
178+
struct decoder_context {
179+
/// @brief Total number of measurements per shot (column count of m2d/m2o).
180+
std::size_t num_measurements() const;
181+
182+
/// @brief Canonicalize X-stabilizer detectors; return decoder_inputs.
183+
decoder_inputs x_component() const;
184+
185+
/// @brief Canonicalize Z-stabilizer detectors; return decoder_inputs.
186+
decoder_inputs z_component() const;
187+
188+
/// @brief Canonicalize both stabilizer types with boundary awareness;
189+
/// return decoder_inputs.
190+
decoder_inputs full_component() const;
191+
192+
private:
193+
cudaq::M2DSparseMatrix m2d_;
194+
cudaq::M2OSparseMatrix m2o_;
195+
detector_error_model dem_;
196+
std::size_t num_rounds_ = 0;
197+
std::size_t num_x_stabilizers_ = 0;
198+
std::size_t num_z_stabilizers_ = 0;
199+
bool fixed_basis_is_z_ = false;
200+
201+
friend decoder_context
202+
decoder_context_from_memory_circuit(const code &, operation, std::size_t,
203+
cudaq::noise_model &, bool);
204+
};
205+
206+
/// @brief Flatten an M2DSparseMatrix into the `-1`-terminated sparse vector a
207+
/// realtime decoder config expects for its `D_sparse`.
208+
std::vector<std::int64_t> d_sparse(const cudaq::M2DSparseMatrix &m2d);
209+
162210
/// @brief Given a memory circuit setup, generate a DEM
163211
/// @param code QEC Code to sample
164212
/// @param statePrep Initial state preparation operation
@@ -167,10 +215,11 @@ sample_memory_circuit(const code &code, std::size_t numShots,
167215
/// @param decompose_errors If true, hyperedge error mechanisms are decomposed
168216
/// into pairs of two-detector edges by Stim before returning.
169217
/// @return Detector error model
170-
cudaq::qec::detector_error_model
171-
dem_from_memory_circuit(const code &code, operation statePrep,
172-
std::size_t numRounds, cudaq::noise_model &noise,
173-
bool decompose_errors = false);
218+
detector_error_model dem_from_memory_circuit(const code &code,
219+
operation statePrep,
220+
std::size_t numRounds,
221+
cudaq::noise_model &noise,
222+
bool decompose_errors = false);
174223

175224
/// @brief Given a memory circuit setup, generate a DEM for X stabilizers.
176225
/// @param code QEC Code to sample
@@ -199,4 +248,13 @@ detector_error_model z_dem_from_memory_circuit(const code &code,
199248
std::size_t numRounds,
200249
cudaq::noise_model &noise,
201250
bool decompose_errors = false);
251+
252+
/// @brief Run a memory-circuit analysis and return a lazy handle.
253+
///
254+
/// Executes `dem_from_kernel` once and stores the raw result. Call
255+
/// `x_component()`, `z_component()`, or `full_component()` on the returned
256+
/// handle to canonicalize exactly the stabilizer type needed.
257+
decoder_context decoder_context_from_memory_circuit(
258+
const code &code, operation statePrep, std::size_t numRounds,
259+
cudaq::noise_model &noise, bool decompose_errors = false);
202260
} // namespace cudaq::qec

0 commit comments

Comments
 (0)