Skip to content

Commit dabaa83

Browse files
authored
Add Parity Check Matrix generation and utility functions (NVIDIA#147)
This PR introduces a CUDA-Q QEC Detector Error Model type (similar to Stim's Detector Error Model but different). A Detector Error Model can be generated from CUDA-Q circuits and then fed to CUDA-Q QEC decoders as inputs so that the decoders know how to decode the syndromes that come from those CUDA-Q circuits. The main implementation and interfaces are done in C++. They are also exposed to Python via pybind bindings. In addition to the core functionality listed above, various PCM utility functions (like column sorting, canonicalizing, combining, etc.) that are used to create the DEM are also exposed to the user. --------- Signed-off-by: Ben Howe <[email protected]>
1 parent 7fc54ec commit dabaa83

File tree

21 files changed

+2511
-28
lines changed

21 files changed

+2511
-28
lines changed

docs/sphinx/api/qec/cpp_api.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ Code
2222
.. doxygenclass:: cudaq::qec::surface_code::surface_code
2323
:members:
2424

25+
Detector Error Model
26+
====================
27+
28+
.. doxygenstruct:: cudaq::qec::detector_error_model
29+
:members:
30+
31+
.. doxygenfunction:: cudaq::qec::dem_from_memory_circuit(const code &, operation, std::size_t, cudaq::noise_model &)
32+
.. doxygenfunction:: cudaq::qec::x_dem_from_memory_circuit(const code &, operation, std::size_t, cudaq::noise_model &)
33+
.. doxygenfunction:: cudaq::qec::z_dem_from_memory_circuit(const code &, operation, std::size_t, cudaq::noise_model &)
2534

2635
Decoder Interfaces
2736
==================
@@ -42,6 +51,21 @@ NVIDIA QLDPC Decoder
4251

4352
.. include:: nv_qldpc_decoder_api.rst
4453

54+
Parity Check Matrix Utilities
55+
=============================
56+
57+
.. doxygenfunction:: cudaq::qec::dense_to_sparse(const cudaqx::tensor<uint8_t> &)
58+
.. doxygenfunction:: cudaq::qec::generate_random_pcm(std::size_t, std::size_t, std::size_t, int, std::mt19937_64 &&);
59+
.. doxygenfunction:: cudaq::qec::get_pcm_for_rounds(const cudaqx::tensor<uint8_t> &, std::uint32_t, std::uint32_t, std::uint32_t, bool, bool);
60+
.. doxygenfunction:: cudaq::qec::get_sorted_pcm_column_indices(const std::vector<std::vector<std::uint32_t>> &, std::uint32_t);
61+
.. doxygenfunction:: cudaq::qec::get_sorted_pcm_column_indices(const cudaqx::tensor<uint8_t> &, std::uint32_t);
62+
.. doxygenfunction:: cudaq::qec::pcm_extend_to_n_rounds(const cudaqx::tensor<uint8_t> &, std::size_t, std::uint32_t);
63+
.. doxygenfunction:: cudaq::qec::pcm_is_sorted(const cudaqx::tensor<uint8_t> &, std::uint32_t);
64+
.. doxygenfunction:: cudaq::qec::reorder_pcm_columns(const cudaqx::tensor<uint8_t> &, const std::vector<std::uint32_t> &, uint32_t, uint32_t);
65+
.. doxygenfunction:: cudaq::qec::shuffle_pcm_columns(const cudaqx::tensor<uint8_t> &, std::mt19937_64 &&);
66+
.. doxygenfunction:: cudaq::qec::simplify_pcm(const cudaqx::tensor<uint8_t> &, const std::vector<double> &, std::uint32_t);
67+
.. doxygenfunction:: cudaq::qec::sort_pcm_columns(const cudaqx::tensor<uint8_t> &, std::uint32_t);
68+
4569
Common
4670
=============
4771

docs/sphinx/api/qec/python_api.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ Code
1010
.. autoclass:: cudaq_qec.Code
1111
:members:
1212

13+
Detector Error Model
14+
====================
15+
16+
.. autoclass:: cudaq_qec.DetectorErrorModel
17+
:members:
18+
19+
.. autofunction:: cudaq_qec.dem_from_memory_circuit
20+
.. autofunction:: cudaq_qec.x_dem_from_memory_circuit
21+
.. autofunction:: cudaq_qec.z_dem_from_memory_circuit
22+
1323
Decoder Interfaces
1424
==================
1525

@@ -35,3 +45,16 @@ Common
3545
.. autofunction:: cudaq_qec.sample_memory_circuit
3646

3747
.. autofunction:: cudaq_qec.sample_code_capacity
48+
49+
Parity Check Matrix Utilities
50+
-------------
51+
52+
.. autofunction:: cudaq_qec.generate_random_pcm
53+
.. autofunction:: cudaq_qec.get_pcm_for_rounds
54+
.. autofunction:: cudaq_qec.get_sorted_pcm_column_indices
55+
.. autofunction:: cudaq_qec.pcm_extend_to_n_rounds
56+
.. autofunction:: cudaq_qec.pcm_is_sorted
57+
.. autofunction:: cudaq_qec.reorder_pcm_columns
58+
.. autofunction:: cudaq_qec.shuffle_pcm_columns
59+
.. autofunction:: cudaq_qec.simplify_pcm
60+
.. autofunction:: cudaq_qec.sort_pcm_columns

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

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@
2727
nShots = 3
2828
nRounds = 4
2929

30-
# error probabily
30+
# Uncomment for repeatability
31+
# cudaq.set_random_seed(13)
32+
33+
# error probability
3134
p = 0.01
3235
noise = cudaq.NoiseModel()
3336
noise.add_all_qubit_channel("x", cudaq.Depolarization2(p), 1)
@@ -37,14 +40,18 @@
3740
# our expected measurement in this state is 0
3841
expected_value = 0
3942

43+
# For large runs, set verbose to False to suppress output
44+
verbose = nShots <= 10
45+
4046
# sample the steane memory circuit with noise on each cx gate
4147
# reading out the syndromes after each stabilizer round (xor'd against the previous)
4248
# and readout out the data qubits at the end of the experiment
4349
syndromes, data = qec.sample_memory_circuit(steane, statePrep, nShots, nRounds,
4450
noise)
45-
print("From sample function:\n")
46-
print("syndromes:\n", syndromes)
47-
print("data:\n", data)
51+
if verbose:
52+
print("From sample function:\n")
53+
print("syndromes:\n", syndromes)
54+
print("data:\n", data)
4855

4956
# Get a decoder
5057
decoder = qec.get_decoder("single_error_lut", H)
@@ -54,7 +61,8 @@
5461
logical_measurements = (Lz @ data.transpose()) % 2
5562
# only one logical qubit, so do not need the second axis
5663
logical_measurements = logical_measurements.flatten()
57-
print("LMz:\n", logical_measurements)
64+
if verbose:
65+
print("LMz:\n", logical_measurements)
5866

5967
# organize data by shot and round if desired
6068
syndromes = syndromes.reshape((nShots, nRounds, syndromes.shape[1]))
@@ -63,33 +71,39 @@
6371
# through the stabilizer rounds
6472
pauli_frame = np.array([0, 0], dtype=np.uint8)
6573
for shot in range(0, nShots):
66-
print("shot:", shot)
74+
if verbose:
75+
print("shot:", shot)
6776
for syndrome in syndromes[shot]:
68-
print("syndrome:", syndrome)
77+
if verbose:
78+
print("syndrome:", syndrome)
6979
# Decode the syndrome
7080
results = decoder.decode(syndrome)
7181
convergence = results.converged
7282
result = results.result
7383
data_prediction = np.array(result, dtype=np.uint8)
7484

7585
# see if the decoded result anti-commutes with the observables
76-
print("decode result:", data_prediction)
86+
if verbose:
87+
print("decode result:", data_prediction)
7788
decoded_observables = (observables @ data_prediction) % 2
78-
print("decoded_observables:", decoded_observables)
89+
if verbose:
90+
print("decoded_observables:", decoded_observables)
7991

8092
# update pauli frame
8193
pauli_frame = (pauli_frame + decoded_observables) % 2
82-
print("pauli frame:", pauli_frame)
94+
if verbose:
95+
print("pauli frame:", pauli_frame)
8396

8497
# after pauli frame has tracked corrections through the rounds
8598
# apply the pauli frame correction to the measurement, and see
8699
# if this matches the state we intended to prepare
87100
# We prepared |0>, so we check if logical measurement Mz + Pf_X = 0
88101
corrected_mz = (logical_measurements[shot] + pauli_frame[0]) % 2
89-
print("Expected value:", expected_value)
90-
print("Corrected value:", corrected_mz)
102+
if verbose:
103+
print("Expected value:", expected_value)
104+
print("Corrected value:", corrected_mz)
91105
if (corrected_mz != expected_value):
92106
nLogicalErrors += 1
93107

94108
# Count how many shots the decoder failed to correct the errors
95-
print("Number of logical errors:", nLogicalErrors)
109+
print(f"Number of logical errors out of {nShots} shots: {nLogicalErrors}")

libs/core/include/cuda-qx/core/heterogeneous_map.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************-*- C++ -*-****
2-
* Copyright (c) 2024 NVIDIA Corporation & Affiliates. *
2+
* Copyright (c) 2024 - 2025 NVIDIA Corporation & Affiliates. *
33
* All rights reserved. *
44
* *
55
* This source code and the accompanying materials are made available under *
@@ -183,6 +183,10 @@ class heterogeneous_map {
183183
/// @return The number of key-value pairs in the map
184184
std::size_t size() const { return items.size(); }
185185

186+
/// @brief Check if the map is empty
187+
/// @return true if the map is empty, false otherwise
188+
bool empty() const { return items.empty(); }
189+
186190
/// @brief Check if the map contains a key
187191
/// @param key The key to check
188192
/// @return true if the key exists, false otherwise

libs/core/include/cuda-qx/core/kwargs_utils.h

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ inline heterogeneous_map hetMapFromKwargs(const py::kwargs &kwargs) {
6767
} else {
6868
throw std::runtime_error("Unsupported array data type in kwargs.");
6969
}
70+
} else if (py::isinstance<py::dict>(value)) {
71+
// Check if it is a kwargs dict
72+
result.insert(key, hetMapFromKwargs(value.cast<py::dict>()));
7073
} else {
7174
throw std::runtime_error(
7275
"Invalid python type for mapping kwargs to a heterogeneous_map.");
@@ -77,14 +80,25 @@ inline heterogeneous_map hetMapFromKwargs(const py::kwargs &kwargs) {
7780
}
7881

7982
template <typename T>
80-
tensor<T> toTensor(const py::array_t<T> &H) {
83+
tensor<T> toTensor(const py::array_t<T> &H, bool perform_pcm_checks = false) {
8184
py::buffer_info buf = H.request();
8285

8386
if (buf.ndim >= 1 && buf.strides[0] == buf.itemsize) {
8487
throw std::runtime_error("toTensor: data must be in row-major order, but "
8588
"column-major order was detected.");
8689
}
8790

91+
if (perform_pcm_checks) {
92+
if (buf.itemsize != sizeof(uint8_t)) {
93+
throw std::runtime_error(
94+
"Parity check matrix must be an array of uint8_t.");
95+
}
96+
97+
if (buf.ndim != 2) {
98+
throw std::runtime_error("Parity check matrix must be 2-dimensional.");
99+
}
100+
}
101+
88102
// Create a vector of the array dimensions
89103
std::vector<std::size_t> shape;
90104
for (py::ssize_t d : buf.shape) {
@@ -96,4 +110,12 @@ tensor<T> toTensor(const py::array_t<T> &H) {
96110
tensor_H.borrow(static_cast<T *>(buf.ptr), shape);
97111
return tensor_H;
98112
}
113+
114+
/// @brief Convert a py::array_t<uint8_t> to a tensor<uint8_t>. This is the same
115+
/// as toTensor, but with additional checks.
116+
template <typename T>
117+
tensor<T> pcmToTensor(const py::array_t<T> &H) {
118+
return toTensor(H, /*perform_pcm_checks=*/true);
119+
}
120+
99121
} // namespace cudaqx

libs/core/include/cuda-qx/core/tensor.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,11 @@ class tensor {
193193
throw std::runtime_error("Dot product requires rank-2 tensors");
194194
}
195195
if (shape()[1] != other.shape()[0]) {
196-
throw std::runtime_error("Invalid matrix dimensions for dot product");
196+
throw std::runtime_error(
197+
"Invalid matrix dimensions for dot product: matrix1 dimensions: " +
198+
std::to_string(shape()[0]) + "x" + std::to_string(shape()[1]) +
199+
", matrix2 dimensions: " + std::to_string(other.shape()[0]) + "x" +
200+
std::to_string(other.shape()[1]));
197201
}
198202

199203
std::vector<std::size_t> result_shape = {shape()[0], other.shape()[1]};
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/****************************************************************-*- C++ -*-****
2+
* Copyright (c) 2025 NVIDIA Corporation & Affiliates. *
3+
* All rights reserved. *
4+
* *
5+
* This source code and the accompanying materials are made available under *
6+
* the terms of the Apache License 2.0 which accompanies this distribution. *
7+
******************************************************************************/
8+
#pragma once
9+
10+
#include "cuda-qx/core/tensor.h"
11+
#include <optional>
12+
13+
namespace cudaq::qec {
14+
15+
/// A detector error model (DEM) for a quantum error correction circuit. A
16+
/// DEM can be created from a QEC circuit and a noise model. It contains
17+
/// information about which errors flip which detectors. This is used by the
18+
/// decoder to help make predictions about observables flips.
19+
///
20+
/// Shared size parameters among the matrix types.
21+
/// - \p detector_error_matrix: num_detectors x num_error_mechanisms [d, e]
22+
/// - \p error_rates: num_error_mechanisms
23+
/// - \p observables_flips_matrix: num_observables x num_error_mechanisms [k, e]
24+
///
25+
/// @note The C++ API for this class may change in the future. The Python API is
26+
/// more likely to be backwards compatible.
27+
struct detector_error_model {
28+
/// The detector error matrix is a specific kind of circuit-level parity-check
29+
/// matrix where each row represents a detector, and each column represents
30+
/// an error mechanism. The entries of this matrix are H[i,j] = 1 if detector
31+
/// i is triggered by error mechanism j, and 0 otherwise.
32+
cudaqx::tensor<uint8_t> detector_error_matrix;
33+
34+
/// The list of weights has length equal to the number of columns of
35+
/// \p detector_error_matrix, which assigns a likelihood to each error
36+
/// mechanism.
37+
std::vector<double> error_rates;
38+
39+
/// The observables flips matrix is a specific kind of circuit-level parity-
40+
/// check matrix where each row represents a Pauli observable, and each
41+
/// column represents an error mechanism. The entries of this matrix are
42+
/// O[i,j] = 1 if Pauli observable i is flipped by error mechanism j, and 0
43+
/// otherwise.
44+
cudaqx::tensor<uint8_t> observables_flips_matrix;
45+
46+
/// Error mechanism ID. From a probability perspective, each error mechanism
47+
/// ID is independent of all other error mechanism ID. For all errors with
48+
/// the *same* ID, only one of them can happen. That is - the errors
49+
/// containing the same ID are correlated with each other.
50+
std::optional<std::vector<std::size_t>> error_ids;
51+
52+
/// Return the number of rows in the detector_error_matrix.
53+
std::size_t num_detectors() const;
54+
55+
/// Return the number of columns in the detector_error_matrix, error_rates,
56+
/// and observables_flips_matrix.
57+
std::size_t num_error_mechanisms() const;
58+
59+
/// Return the number of rows in the observables_flips_matrix.
60+
std::size_t num_observables() const;
61+
62+
/// Put the detector_error_matrix into canonical form, where the rows and
63+
/// columns are ordered in a way that is amenable to the round-based decoding
64+
/// process.
65+
void canonicalize_for_rounds(uint32_t num_syndromes_per_round);
66+
};
67+
68+
} // namespace cudaq::qec

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************-*- C++ -*-****
2-
* Copyright (c) 2024 NVIDIA Corporation & Affiliates. *
2+
* Copyright (c) 2024 - 2025 NVIDIA Corporation & Affiliates. *
33
* All rights reserved. *
44
* *
55
* This source code and the accompanying materials are made available under *
@@ -8,6 +8,7 @@
88
#pragma once
99

1010
#include "cudaq/qec/code.h"
11+
#include "cudaq/qec/detector_error_model.h"
1112

1213
namespace cudaq::qec {
1314

@@ -100,4 +101,36 @@ sample_memory_circuit(const code &code, std::size_t numShots,
100101
std::tuple<cudaqx::tensor<uint8_t>, cudaqx::tensor<uint8_t>>
101102
sample_memory_circuit(const code &code, std::size_t numShots,
102103
std::size_t numRounds, cudaq::noise_model &noise);
104+
105+
/// @brief Given a memory circuit setup, generate a DEM
106+
/// @param code QEC Code to sample
107+
/// @param statePrep Initial state preparation operation
108+
/// @param numRounds Number of stabilizer measurement rounds
109+
/// @param noise Noise model to apply
110+
/// @return Detector error model
111+
cudaq::qec::detector_error_model
112+
dem_from_memory_circuit(const code &code, operation statePrep,
113+
std::size_t numRounds, cudaq::noise_model &noise);
114+
115+
/// @brief Given a memory circuit setup, generate a DEM for X stabilizers.
116+
/// @param code QEC Code to sample
117+
/// @param statePrep Initial state preparation operation
118+
/// @param numRounds Number of stabilizer measurement rounds
119+
/// @param noise Noise model to apply
120+
/// @return Detector error model
121+
detector_error_model x_dem_from_memory_circuit(const code &code,
122+
operation statePrep,
123+
std::size_t numRounds,
124+
cudaq::noise_model &noise);
125+
126+
/// @brief Given a memory circuit setup, generate a DEM for Z stabilizers.
127+
/// @param code QEC Code to sample
128+
/// @param statePrep Initial state preparation operation
129+
/// @param numRounds Number of stabilizer measurement rounds
130+
/// @param noise Noise model to apply
131+
/// @return Detector error model
132+
detector_error_model z_dem_from_memory_circuit(const code &code,
133+
operation statePrep,
134+
std::size_t numRounds,
135+
cudaq::noise_model &noise);
103136
} // namespace cudaq::qec

0 commit comments

Comments
 (0)