diff --git a/docs/sphinx/api/qec/cpp_api.rst b/docs/sphinx/api/qec/cpp_api.rst index fd752c534..57f953c27 100644 --- a/docs/sphinx/api/qec/cpp_api.rst +++ b/docs/sphinx/api/qec/cpp_api.rst @@ -44,6 +44,25 @@ Decoder Interfaces .. doxygenstruct:: cudaq::qec::decoder_result :members: +Sparse Parity-Check Matrices +============================ + +``sparse_binary_matrix`` stores a binary matrix in compressed sparse column +(CSC) or compressed sparse row (CSR) layout without storing values for its +nonzero entries. Input indices are preserved as supplied. Use +:cpp:func:`cudaq::qec::sparse_binary_matrix::canonicalize` when duplicate +indices should be combined over GF(2) and each compressed row or column should +be sorted. Canonicalization preserves the matrix layout. + +The matrix uses ``std::uint32_t`` indices, so each dimension and the number of +stored entries must fit in that type. Dense ``cudaqx::tensor`` +PCMs remain accepted by decoder entry points through an implicit conversion. + +.. doxygenenum:: cudaq::qec::sparse_binary_matrix_layout + +.. doxygenclass:: cudaq::qec::sparse_binary_matrix + :members: + Built-in Decoders ================= @@ -78,19 +97,33 @@ Real-Time Decoding Parity Check Matrix Utilities ============================= +The overloads that accept ``sparse_binary_matrix`` operate without +materializing the full input as a dense tensor. Use +:cpp:func:`cudaq::qec::generate_random_pcm_sparse` when a generated PCM would +be impractical to allocate densely. The dense generator remains available and +rejects dimensions whose products overflow ``std::size_t``. + .. doxygenfunction:: cudaq::qec::dense_to_sparse(const cudaqx::tensor &) .. doxygenfunction:: cudaq::qec::generate_random_pcm(std::size_t, std::size_t, std::size_t, int, std::mt19937_64 &&); +.. doxygenfunction:: cudaq::qec::generate_random_pcm_sparse(std::size_t, std::size_t, std::size_t, int, std::mt19937_64 &&); .. doxygenfunction:: cudaq::qec::generate_timelike_sparse_detector_matrix(std::uint32_t num_syndromes_per_round, std::uint32_t num_rounds, bool include_first_round = false) .. doxygenfunction:: cudaq::qec::generate_timelike_sparse_detector_matrix(std::uint32_t num_syndromes_per_round, std::uint32_t num_rounds, std::vector first_round_matrix) .. doxygenfunction:: cudaq::qec::get_pcm_for_rounds(const cudaqx::tensor &, std::uint32_t, std::uint32_t, std::uint32_t, bool, bool); +.. doxygenfunction:: cudaq::qec::get_pcm_for_rounds(const sparse_binary_matrix &, std::uint32_t, std::uint32_t, std::uint32_t, bool, bool, bool); .. doxygenfunction:: cudaq::qec::get_sorted_pcm_column_indices(const std::vector> &, std::uint32_t); .. doxygenfunction:: cudaq::qec::get_sorted_pcm_column_indices(const cudaqx::tensor &, std::uint32_t); .. doxygenfunction:: cudaq::qec::pcm_extend_to_n_rounds(const cudaqx::tensor &, std::size_t, std::uint32_t); +.. doxygenfunction:: cudaq::qec::pcm_from_sparse_string(const std::string &, std::size_t, std::size_t) .. doxygenfunction:: cudaq::qec::pcm_from_sparse_vec(const std::vector& sparse_vec, std::size_t num_rows, std::size_t num_cols) .. doxygenfunction:: cudaq::qec::pcm_is_sorted(const cudaqx::tensor &, std::uint32_t); +.. doxygenfunction:: cudaq::qec::pcm_to_sparse_string(const cudaqx::tensor &) +.. doxygenfunction:: cudaq::qec::pcm_to_sparse_string(const sparse_binary_matrix &) .. doxygenfunction:: cudaq::qec::pcm_to_sparse_vec(const cudaqx::tensor& pcm) +.. doxygenfunction:: cudaq::qec::pcm_to_sparse_vec(const sparse_binary_matrix &) .. doxygenfunction:: cudaq::qec::reorder_pcm_columns(const cudaqx::tensor &, const std::vector &, uint32_t, uint32_t); +.. doxygenfunction:: cudaq::qec::reorder_pcm_columns(const sparse_binary_matrix &, const std::vector &, uint32_t, uint32_t); .. doxygenfunction:: cudaq::qec::shuffle_pcm_columns(const cudaqx::tensor &, std::mt19937_64 &&); +.. doxygenfunction:: cudaq::qec::shuffle_pcm_columns(const sparse_binary_matrix &, std::mt19937_64 &&); .. doxygenfunction:: cudaq::qec::simplify_pcm(const cudaqx::tensor &, const std::vector &, std::uint32_t); .. doxygenfunction:: cudaq::qec::sort_pcm_columns(const cudaqx::tensor &, std::uint32_t); diff --git a/docs/sphinx/components/qec/introduction.rst b/docs/sphinx/components/qec/introduction.rst index d59853b63..b61e07fc7 100644 --- a/docs/sphinx/components/qec/introduction.rst +++ b/docs/sphinx/components/qec/introduction.rst @@ -524,7 +524,7 @@ The decoder base class defines the core interface for syndrome decoding: protected: std::size_t block_size; // For [n,k] code, this is n std::size_t syndrome_size; // For [n,k] code, this is n-k - tensor H; // Parity check matrix + sparse_binary_matrix H; // Parity check matrix public: struct decoder_result { @@ -541,7 +541,7 @@ The decoder base class defines the core interface for syndrome decoding: Key Components: -* **Parity Check Matrix**: Defines the code structure via :code:`H` +* **Parity Check Matrix**: Defines the code structure via the sparse :code:`H` member * **Block Size**: Number of physical qubits in the code * **Syndrome Size**: Number of stabilizer measurements * **Decoder Result**: Contains convergence status and error probabilities @@ -561,7 +561,7 @@ To implement a new decoder: // Decoder-specific members public: - my_decoder(const tensor& H, + my_decoder(const qec::sparse_binary_matrix& H, const heterogeneous_map& params) : decoder(H) { // Initialize decoder @@ -580,14 +580,19 @@ To implement a new decoder: CUDAQ_EXTENSION_CUSTOM_CREATOR_FUNCTION( my_decoder, static std::unique_ptr create( - const tensor& H, + const qec::decoder_init& init, const heterogeneous_map& params) { - return std::make_unique(H, params); + return qec::make_pcm_decoder(init, params); } ) CUDAQ_EXT_PT_REGISTER_TYPE(my_decoder) +The :code:`make_pcm_decoder` helper dispatches :code:`decoder_init`. It +passes a stored sparse PCM directly to the decoder constructor; when the +variant contains Stim DEM text, it parses the DEM and constructs the sparse +detector matrix before invoking the same constructor. + Example: Lookup Table Decoder ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -600,18 +605,17 @@ Here's a simple lookup table decoder for the Steane code: std::map single_qubit_err_signatures; public: - single_error_lut(const tensor& H, + single_error_lut(const qec::sparse_binary_matrix& H, const heterogeneous_map& params) : decoder(H) { - // Build lookup table for single-qubit errors + // Canonicalize before using each sparse column as an error + // signature so duplicate row indices cancel over GF(2). + auto H_e2d = H.canonicalize().to_nested_csc(); + for (std::size_t qErr = 0; qErr < block_size; qErr++) { std::string err_sig(syndrome_size, '0'); - for (std::size_t r = 0; r < syndrome_size; r++) { - bool syndrome = 0; - for (std::size_t c = 0; c < block_size; c++) - syndrome ^= (c != qErr) && H.at({r, c}); - err_sig[r] = syndrome ? '1' : '0'; - } + for (std::uint32_t row : H_e2d[qErr]) + err_sig[row] = '1'; single_qubit_err_signatures.insert({err_sig, qErr}); } } diff --git a/docs/sphinx/examples_rst/qec/decoders.rst b/docs/sphinx/examples_rst/qec/decoders.rst index 0e9bb3821..e9084c4e3 100644 --- a/docs/sphinx/examples_rst/qec/decoders.rst +++ b/docs/sphinx/examples_rst/qec/decoders.rst @@ -16,6 +16,13 @@ that would result from those errors. ``H``. This is the preferred form for large PCMs because no dense ``rows x cols`` allocation is made — the matrix is normalised to CSR internally. Dense NumPy ``uint8`` arrays remain supported. + The PCM utilities :func:`cudaq_qec.reorder_pcm_columns`, + :func:`cudaq_qec.shuffle_pcm_columns`, and + :func:`cudaq_qec.pcm_to_sparse_vec` also accept SciPy sparse matrices + without creating a dense ``cudaqx::tensor``. Reordering and shuffling a + sparse input returns a ``scipy.sparse.csc_matrix``; a dense input continues + to return a NumPy array. + ``scipy`` is an optional dependency; if it is not installed, pass a dense NumPy array instead.