Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions libs/qec/include/cudaq/qec/code.h
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,47 @@ class code : public cudaqx::extension_point<code, const heterogeneous_map &> {
/// @return Tensor representing Lz
cudaqx::tensor<uint8_t> get_observables_z() const;

/// @brief Get the inlined feedback matrix for detector construction
///
/// Shape is [numCols x numCols], where numCols =
/// get_num_ancilla_qubits() (one measurement record per ancilla per
/// round, in [Z][X] order). Entry (j, k) = 1 means the cross-round
/// detector comparing record j between consecutive rounds additionally
/// XORs record k of the earlier round, and the final boundary detector
/// for record j additionally XORs record k of the last round.
/// @return Tensor representing the inlined feedback matrix. An empty
/// tensor (the default) means no feedback is applied and detectors are
/// formed from same-record comparisons only.
virtual cudaqx::tensor<uint8_t> get_inlined_feedback() const;

/// @brief Get the inlined feedback matrix for logical observables measured
/// in the Z basis
///
/// Shape is [num_observables x numCols], where numCols =
/// get_num_ancilla_qubits() (one measurement record per ancilla per
/// round, in [Z][X] order). Entry (m, k) = 1 means logical observable m
/// additionally XORs record k of every round. This getter is consumed when
/// the memory experiment measures its observables in the Z basis (prep0 /
/// prep1 state preparations).
/// @return Tensor representing the Z-basis observable inlined feedback
/// matrix. An empty tensor (the default) means no observable feedback is
/// applied in the Z basis.
virtual cudaqx::tensor<uint8_t> get_observable_inlined_feedback_z() const;

/// @brief Get the inlined feedback matrix for logical observables measured
/// in the X basis
///
/// Shape is [num_observables x numCols], where numCols =
/// get_num_ancilla_qubits() (one measurement record per ancilla per
/// round, in [Z][X] order). Entry (m, k) = 1 means logical observable m
/// additionally XORs record k of every round. This getter is consumed when
/// the memory experiment measures its observables in the X basis (prepp /
/// prepm state preparations).
/// @return Tensor representing the X-basis observable inlined feedback
/// matrix. An empty tensor (the default) means no observable feedback is
/// applied in the X basis.
virtual cudaqx::tensor<uint8_t> get_observable_inlined_feedback_x() const;

/// @brief Get the stabilizer generators
/// @return Reference to stabilizers
const std::vector<cudaq::spin_op_term> &get_stabilizers() const {
Expand Down
43 changes: 43 additions & 0 deletions libs/qec/include/cudaq/qec/detector_error_model.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,49 @@ struct detector_error_model {
bool remove_zero_syndrome_errors = false);
};

/// @brief Record-to-detector composition layout derived from a code's
/// declared inlined-feedback matrices (CSR-style).
///
/// Entry range [detector_offsets[j], detector_offsets[j+1]) of
/// detector_indices lists the herald record columns XOR-ed into record j's
/// cross-round detector (records of the earlier round) and into its final
/// boundary detector (records of the last round). Entry range
/// [observable_offsets[m], observable_offsets[m+1]) of observable_indices
/// lists the record columns XOR-ed into logical observable m on every round.
/// Empty offsets vectors mean no feedback is declared for that target
/// (identity detector/observable structure).
struct inlined_feedback_layout {
std::vector<std::size_t> detector_indices;
std::vector<std::size_t> detector_offsets;
std::vector<std::size_t> observable_indices;
std::vector<std::size_t> observable_offsets;
};

/// @brief Build the record-to-detector composition layout from a code's
/// declared inlined-feedback matrices.
///
/// @param feedback Detector feedback matrix, shape
/// [num_syndromes_per_round x num_syndromes_per_round] with 0/1
/// entries, or an empty tensor for none. Entry (j, k) = 1 means the
/// cross-round detector for record j additionally XORs record k of
/// the earlier round, and the final boundary detector for record j
/// additionally XORs record k of the last round.
/// @param obs_feedback Observable feedback matrix, shape
/// [num_observables x num_syndromes_per_round] with 0/1 entries, or
/// an empty tensor for none. Entry (m, k) = 1 means logical
/// observable m additionally XORs record k of every round.
/// @param num_syndromes_per_round Records per round (number of ancilla
/// qubits).
/// @param num_observables Number of logical observables.
/// @return The CSR layout; see inlined_feedback_layout.
/// @throws std::runtime_error if a non-empty tensor's shape does not match
/// the expected dimensions.
inlined_feedback_layout
build_inlined_feedback_layout(const cudaqx::tensor<uint8_t> &feedback,
const cudaqx::tensor<uint8_t> &obs_feedback,
std::size_t num_syndromes_per_round,
std::size_t num_observables);

/// Parse the Stim DEM string @p dem_text into detector/observable flip
/// matrices and error rates. DEM-native decoders should consume raw DEM text
/// instead. By default (@p use_decomp_suggestions = false) the '^' separators
Expand Down
12 changes: 12 additions & 0 deletions libs/qec/lib/code.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ cudaqx::tensor<uint8_t> code::get_observables_z() const {
return to_parity_matrix(m_pauli_observables, stabilizer_type::Z);
}

cudaqx::tensor<uint8_t> code::get_inlined_feedback() const {
return cudaqx::tensor<uint8_t>();
}

cudaqx::tensor<uint8_t> code::get_observable_inlined_feedback_z() const {
return cudaqx::tensor<uint8_t>();
}

cudaqx::tensor<uint8_t> code::get_observable_inlined_feedback_x() const {
return cudaqx::tensor<uint8_t>();
}

std::unique_ptr<code> get_code(const std::string &name,
const std::vector<cudaq::spin_op_term> &stab,
const heterogeneous_map options) {
Expand Down
45 changes: 45 additions & 0 deletions libs/qec/lib/detector_error_model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -296,4 +296,49 @@ void detector_error_model::canonicalize_for_rounds(
this->observables_flips_matrix, final_column_order);
}

/// @brief Convert one 0/1 feedback matrix into a CSR row layout. An empty
/// tensor (the identity default) leaves @p indices and @p offsets empty; a
/// non-empty tensor must have shape [num_rows, num_cols].
static void feedback_rows_to_csr(const cudaqx::tensor<uint8_t> &matrix,
std::size_t num_rows, std::size_t num_cols,
const char *name,
std::vector<std::size_t> &indices,
std::vector<std::size_t> &offsets) {
if (matrix.rank() == 0 || matrix.size() == 0)
return;
if (matrix.rank() != 2 || matrix.shape()[0] != num_rows ||
matrix.shape()[1] != num_cols) {
std::string actual;
for (std::size_t d = 0; d < matrix.rank(); ++d)
actual += (d ? ", " : "") + std::to_string(matrix.shape()[d]);
throw std::runtime_error(
std::string(name) + " has invalid shape [" + actual + "] - expected [" +
std::to_string(num_rows) + ", " + std::to_string(num_cols) +
"] or an empty tensor.");
}
offsets.resize(num_rows + 1);
offsets[0] = 0;
for (std::size_t r = 0; r < num_rows; ++r) {
for (std::size_t c = 0; c < num_cols; ++c)
if (matrix.at({r, c}) != 0)
indices.push_back(c);
offsets[r + 1] = indices.size();
}
}

inlined_feedback_layout
build_inlined_feedback_layout(const cudaqx::tensor<uint8_t> &feedback,
const cudaqx::tensor<uint8_t> &obs_feedback,
std::size_t num_syndromes_per_round,
std::size_t num_observables) {
inlined_feedback_layout layout;
feedback_rows_to_csr(feedback, num_syndromes_per_round,
num_syndromes_per_round, "inlined feedback",
layout.detector_indices, layout.detector_offsets);
feedback_rows_to_csr(obs_feedback, num_observables, num_syndromes_per_round,
"observable inlined feedback", layout.observable_indices,
layout.observable_offsets);
return layout;
}

} // namespace cudaq::qec
161 changes: 161 additions & 0 deletions libs/qec/lib/device/inlined_feedback.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/****************************************************************-*- C++ -*-****
* Copyright (c) 2024 - 2025 NVIDIA Corporation & Affiliates. *
* All rights reserved. *
* *
* This source code and the accompanying materials are made available under *
* the terms of the Apache License 2.0 which accompanies this distribution. *
******************************************************************************/

#pragma once

#include "cudaq.h"

namespace cudaq::qec {

/// @brief Number of nonzero entries in row @p row of a row-major 0/1 matrix
/// that has @p num_cols columns (i.e. the weight of one feedback/observable/
/// stabilizer row).
inline __qpu__ std::size_t
row_support_weight(const std::vector<std::size_t> &matrix_flat, std::size_t row,
std::size_t num_cols) {
std::size_t weight = 0;
for (std::size_t k = 0; k < num_cols; ++k)
if (matrix_flat[row * num_cols + k] != 0)
weight++;
return weight;
}

/// @brief Records for the cross-round detector of syndrome record @p j: the
/// earlier-vs-current comparison `{prev[j], curr[j]}` followed by the
/// earlier-round herald records `prev[k]` for each column k (ascending) with
/// `feedback_flat(j, k) != 0`.
inline __qpu__ std::vector<cudaq::measure_result> cross_round_detector_records(
const std::vector<cudaq::measure_result> &prev,
const std::vector<cudaq::measure_result> &curr, std::size_t j,
const std::vector<std::size_t> &feedback_flat, std::size_t num_cols) {
std::size_t weight = row_support_weight(feedback_flat, j, num_cols);
std::vector<cudaq::measure_result> det(2 + weight);
det[0] = prev[j];
det[1] = curr[j];
std::size_t idx = 2;
for (std::size_t k = 0; k < num_cols; ++k)
if (feedback_flat[j * num_cols + k] != 0)
det[idx++] = prev[k];
return det;
}

/// @brief Records for the boundary detector of syndrome record @p record_row:
/// `{last_syndrome[record_row]}`, then the data-qubit readouts
/// `data_results[q]` for each data qubit q (ascending) in the stabilizer
/// support `stabilizers[row_base + q]`, then the last-round herald records
/// `last_syndrome[k]` for each column k (ascending) with
/// `feedback_flat(record_row, k) != 0`. When @p feedback_flat is empty the
/// herald part contributes nothing, so one helper serves both the
/// feedback and legacy branches.
inline __qpu__ std::vector<cudaq::measure_result> boundary_detector_records(
const std::vector<cudaq::measure_result> &last_syndrome,
std::size_t record_row,
const std::vector<cudaq::measure_result> &data_results,
const std::vector<std::size_t> &stabilizers, std::size_t row_base,
std::size_t num_data, const std::vector<std::size_t> &feedback_flat,
std::size_t num_cols) {
std::size_t support_weight = 0;
for (std::size_t q = 0; q < num_data; ++q)
if (stabilizers[row_base + q] != 0)
support_weight++;
std::size_t fb_weight =
feedback_flat.size() > 0
? row_support_weight(feedback_flat, record_row, num_cols)
: 0;
std::vector<cudaq::measure_result> support(1 + support_weight + fb_weight);
support[0] = last_syndrome[record_row];
std::size_t idx = 1;
for (std::size_t q = 0; q < num_data; ++q)
if (stabilizers[row_base + q] != 0)
support[idx++] = data_results[q];
if (feedback_flat.size() > 0)
for (std::size_t k = 0; k < num_cols; ++k)
if (feedback_flat[record_row * num_cols + k] != 0)
support[idx++] = last_syndrome[k];
return support;
}

/// @brief Round-major slice offsets into the observable-feedback buffer. Entry
/// m is the start index of observable m's slice; each observable occupies
/// `num_rounds * (weight of obs_feedback_flat row m)` records. The returned
/// vector has `num_observables + 1` entries; the last entry is the total
/// buffer size. Returns an empty vector when @p obs_feedback_flat is empty
/// (no observable feedback declared).
inline __qpu__ std::vector<std::size_t>
observable_feedback_offsets(const std::vector<std::size_t> &obs_feedback_flat,
std::size_t num_observables, std::size_t num_cols,
std::size_t num_rounds) {
// Sized (not default) construction: the __qpu__ dialect forbids the default
// std::vector constructor. An empty obs_feedback_flat yields a size-0 vector.
std::size_t num_offsets =
obs_feedback_flat.size() > 0 ? num_observables + 1 : 0;
std::vector<std::size_t> offsets(num_offsets);
if (obs_feedback_flat.size() == 0)
return offsets;
std::size_t total = 0;
for (std::size_t m = 0; m < num_observables; ++m) {
offsets[m] = total;
total += num_rounds * row_support_weight(obs_feedback_flat, m, num_cols);
}
offsets[num_observables] = total;
return offsets;
}

/// @brief Write the records that observable feedback collects during @p round
/// into the round-major @p obs_fb_records buffer. For each observable m, the
/// round-r block starts at `offsets[m] + round * (weight of obs_feedback_flat
/// row m)` and holds `syndrome[k]` for each column k (ascending) with
/// `obs_feedback_flat(m, k) != 0`. Call with round 0 for the first round's
/// syndrome and with the running round index for each subsequent round.
inline __qpu__ void collect_observable_feedback_round(
std::vector<cudaq::measure_result> &obs_fb_records,
const std::vector<std::size_t> &offsets,
const std::vector<cudaq::measure_result> &syndrome, std::size_t round,
const std::vector<std::size_t> &obs_feedback_flat,
std::size_t num_observables, std::size_t num_cols) {
for (std::size_t m = 0; m < num_observables; ++m) {
std::size_t weight = row_support_weight(obs_feedback_flat, m, num_cols);
std::size_t idx = offsets[m] + round * weight;
for (std::size_t k = 0; k < num_cols; ++k)
if (obs_feedback_flat[m * num_cols + k] != 0)
obs_fb_records[idx++] = syndrome[k];
}
}

/// @brief Support records for logical observable @p obs: the feedback records
/// collected in @p obs_fb_records (round-major, occupying
/// `[offsets[obs], offsets[obs + 1])`), followed by the data-qubit readouts
/// `data_results[q]` for each data qubit q (ascending) with
/// `obs_matrix_flat(obs, q) != 0`. When @p offsets is empty (no observable
/// feedback declared) the feedback prefix is empty and only the data support
/// is returned.
inline __qpu__ std::vector<cudaq::measure_result> observable_support_records(
std::size_t obs, const std::vector<std::size_t> &obs_matrix_flat,
std::size_t num_data,
const std::vector<cudaq::measure_result> &data_results,
const std::vector<cudaq::measure_result> &obs_fb_records,
const std::vector<std::size_t> &offsets) {
std::size_t support_weight =
row_support_weight(obs_matrix_flat, obs, num_data);
std::size_t fb_count = 0;
std::size_t base = 0;
if (offsets.size() > 0) {
base = offsets[obs];
fb_count = offsets[obs + 1] - offsets[obs];
}
std::vector<cudaq::measure_result> obs_support(fb_count + support_weight);
for (std::size_t i = 0; i < fb_count; ++i)
obs_support[i] = obs_fb_records[base + i];
std::size_t idx = fb_count;
for (std::size_t q = 0; q < num_data; ++q)
if (obs_matrix_flat[obs * num_data + q] != 0)
obs_support[idx++] = data_results[q];
return obs_support;
}

} // namespace cudaq::qec
Loading
Loading