Skip to content

Commit a60eb71

Browse files
committed
DynDEM: validation fixes and updated tests
Signed-off-by: Tracy Shannon <tshannon@nvidia.com>
1 parent 11b55f0 commit a60eb71

12 files changed

Lines changed: 783 additions & 85 deletions

File tree

libs/qec/include/cudaq/qec/code_matrices.h

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,69 @@ inline void check_num_cols(const sparse_binary_matrix &m, std::size_t n,
115115
") does not match n_qubits (" + std::to_string(n) + ")");
116116
}
117117

118-
/// Throw if per_qubit is non-empty with a length that does not match n.
118+
/// Throw if rates is non-empty with a length that does not match n.
119+
/// size_label names the dimension n counts, so a per-check vector does not
120+
/// report a per-qubit mismatch.
121+
inline void check_rate_vector_size(const std::vector<double> &rates,
122+
std::size_t n, const char *label,
123+
const char *size_label) {
124+
if (!rates.empty() && rates.size() != n)
125+
throw std::invalid_argument(std::string(label) + " has " +
126+
std::to_string(rates.size()) + " entries but " +
127+
size_label + "=" + std::to_string(n));
128+
}
129+
130+
/// Throw if per_qubit is non-empty with a length that does not match n_qubits.
119131
inline void check_per_qubit_size(const std::vector<double> &per_qubit,
120132
std::size_t n, const char *label) {
121-
if (!per_qubit.empty() && per_qubit.size() != n)
122-
throw std::invalid_argument(std::string(label) + " has " +
123-
std::to_string(per_qubit.size()) +
124-
" entries but n_qubits=" + std::to_string(n));
133+
check_rate_vector_size(per_qubit, n, label, "n_qubits");
134+
}
135+
136+
/// Throw unless p is a probability. Written as a negated range test so that
137+
/// NaN, which compares false against everything, is rejected too.
138+
inline void check_probability(double p, const std::string &label) {
139+
if (!(p >= 0.0 && p <= 1.0))
140+
throw std::invalid_argument(label + " must be a probability in [0, 1]," +
141+
" got " + std::to_string(p));
142+
}
143+
144+
/// Throw unless every scalar and per-element rate is a probability. Zero rates
145+
/// are legal and simply produce no DEM column; the point of this check is that
146+
/// a negative or NaN rate is silently inactive under that same rule, so a
147+
/// mistyped configuration would otherwise build a smaller DEM instead of
148+
/// failing.
149+
inline void validate_noise_rates(const css_noise_params &noise) {
150+
check_probability(noise.px, "px");
151+
check_probability(noise.py, "py");
152+
check_probability(noise.pz, "pz");
153+
check_probability(noise.pm, "pm");
154+
155+
const auto check_each = [](const std::vector<double> &rates,
156+
const char *label) {
157+
for (std::size_t i = 0; i < rates.size(); ++i)
158+
check_probability(rates[i],
159+
std::string(label) + "[" + std::to_string(i) + "]");
160+
};
161+
check_each(noise.px_per_qubit, "px_per_qubit");
162+
check_each(noise.py_per_qubit, "py_per_qubit");
163+
check_each(noise.pz_per_qubit, "pz_per_qubit");
164+
check_each(noise.pm_per_check, "pm_per_check");
165+
}
166+
167+
/// True when any noise rate is nonzero, i.e. the model asks for at least one
168+
/// fault mechanism. Call only after validate_noise_rates(), so that a nonzero
169+
/// rate here really is a positive probability.
170+
inline bool has_any_noise(const css_noise_params &noise) {
171+
const auto any_nonzero = [](const std::vector<double> &rates) {
172+
for (const double p : rates)
173+
if (p != 0.0)
174+
return true;
175+
return false;
176+
};
177+
return noise.px != 0.0 || noise.py != 0.0 || noise.pz != 0.0 ||
178+
noise.pm != 0.0 || any_nonzero(noise.px_per_qubit) ||
179+
any_nonzero(noise.py_per_qubit) || any_nonzero(noise.pz_per_qubit) ||
180+
any_nonzero(noise.pm_per_check);
125181
}
126182

127183
/// Nested CSC for m padded to outer size n; new entries are empty vectors.

libs/qec/include/cudaq/qec/dem_construction.h

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@
77
******************************************************************************/
88

99
// This file declares dem_from_css_matrices(), which builds a T-round
10-
// code-capacity detector_error_model directly from CSS generator matrices
11-
// and a depolarizing noise model, without requiring a stabilizer circuit or
12-
// Stim round-trip. num_rounds defaults to 1 for the single-round case.
10+
// detector_error_model directly from CSS generator matrices and a noise
11+
// model, without requiring a stabilizer circuit or Stim round-trip.
12+
// num_rounds defaults to 1 for the single-round case.
13+
//
14+
// The model is code-capacity when only data-qubit rates are set, and
15+
// phenomenological when a measurement error rate (pm / pm_per_check) is set
16+
// as well, which adds one fault column per active check per round.
1317

1418
#pragma once
1519

@@ -18,8 +22,8 @@
1822

1923
namespace cudaq::qec {
2024

21-
/// @brief Build a T-round code-capacity detector_error_model from CSS
22-
/// generator matrices and a depolarizing noise model.
25+
/// @brief Build a T-round detector_error_model from CSS generator matrices
26+
/// and a depolarizing (optionally phenomenological) noise model.
2327
///
2428
/// Models T independent syndrome measurement rounds. Detectors are syndrome
2529
/// *differences* between consecutive rounds: detector[r] fires when the
@@ -36,29 +40,35 @@ namespace cudaq::qec {
3640
/// @param code CSS code matrices. All non-empty matrices must share the
3741
/// same num_cols() value (n_qubits). Default-constructed
3842
/// (zero-row, zero-column) matrices are treated as empty.
39-
/// @param noise Depolarizing noise rates applied identically to every
40-
/// round. Per-qubit vectors override the scalar rates;
41-
/// qubits with effective rate 0 produce no column.
43+
/// @param noise Noise rates applied identically to every round.
44+
/// Per-element vectors override the scalar rates; elements
45+
/// with effective rate 0 produce no column.
4246
/// @param num_rounds Number of syndrome measurement rounds T (default 1,
4347
/// must be >= 1).
4448
/// @return detector_error_model with:
4549
/// - detector_error_matrix: [T*d x e] where
4650
/// d = hz.num_rows() + hx.num_rows(),
47-
/// e = T * (|active_X| + |active_Z| + |active_Y|).
51+
/// e = T * (|active_X| + |active_Z| + |active_Y| + |active_checks|).
52+
/// |active_checks| is the number of checks with a nonzero measurement
53+
/// error rate, and is 0 unless pm or pm_per_check is set.
4854
/// Round r occupies rows r*d .. (r+1)*d-1. Within each round:
4955
/// rows 0 .. hz.num_rows()-1 are Z-type detectors (X and Y faults);
5056
/// rows hz.num_rows() .. d-1 are X-type detectors (Z and Y faults).
57+
/// A measurement-error column touches the one detector row of its own
58+
/// check in rounds r and r+1, whichever type that check is.
5159
/// - observables_flips_matrix: [k x e],
5260
/// k = lz.num_rows() + lx.num_rows().
5361
/// Faults in any round flip the same observable rows (the logical
54-
/// measurement is taken once at the end of the experiment).
62+
/// measurement is taken once at the end of the experiment). Measurement
63+
/// errors flip no observable.
5564
/// - error_rates: column layout is
5665
/// [round 0 faults | ... | round T-1 faults]; within each round:
57-
/// [active X qubits | active Z qubits | active Y qubits], each in
58-
/// ascending qubit-index order.
66+
/// [active X qubits | active Z qubits | active Y qubits |
67+
/// active checks], each in ascending index order.
5968
/// @throws std::invalid_argument if num_rounds is 0, if two non-empty
60-
/// matrices have inconsistent num_cols() values, or if a per-qubit
61-
/// rate vector length does not equal n_qubits.
69+
/// matrices have inconsistent num_cols() values, if a per-qubit rate
70+
/// vector length does not equal n_qubits, if pm_per_check's length
71+
/// does not equal d, or if any rate is not a probability in [0, 1].
6272
detector_error_model dem_from_css_matrices(const css_code_matrices &code,
6373
const css_noise_params &noise,
6474
std::size_t num_rounds = 1);

libs/qec/include/cudaq/qec/extended_dem.h

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,23 @@ struct extended_dem {
8787

8888
/// Number of outgoing-seam rows, i.e. rows of out_syndrome.
8989
uint32_t num_out_seam_rows() const;
90+
91+
/// @brief Throw std::invalid_argument unless this chunk is internally
92+
/// consistent, i.e. safe to stitch, close, or merge.
93+
///
94+
/// num_faults() reports in_syndrome's width alone, so nothing else about a
95+
/// chunk is self-describing: a block that disagrees with it, or a prior list
96+
/// that does not have one entry per fault, would otherwise be read at the
97+
/// wrong width and silently misalign columns (or walk past the end of a
98+
/// nested column list). Checks that
99+
/// - every block is num_faults() columns wide, including zero-row blocks
100+
/// (use a width-n empty matrix, not a default-constructed one, when the
101+
/// chunk has faults),
102+
/// - fault_priors has one entry per fault, and
103+
/// - each tag vector has one entry per row of the seam it names.
104+
///
105+
/// @param context Prefix for the error message, naming the caller.
106+
void validate(const char *context) const;
90107
};
91108

92109
/// @brief Build a one-round extended_dem from CSS matrices and noise.
@@ -141,9 +158,10 @@ struct dem_chunk_spec {
141158
/// True when nothing has been set, used to detect an omitted phase.
142159
bool is_empty() const;
143160

144-
/// @brief Check internal consistency: a positive fault count, one error rate
145-
/// per fault with each in [0, 1], and index lists that are -1 terminated
146-
/// with every index in [0, num_faults).
161+
/// @brief Check internal consistency: a positive fault count that fits in
162+
/// uint32_t (sparse matrix column index width), one error rate per fault with
163+
/// each in [0, 1], and index lists that are -1 terminated with every index
164+
/// in [0, num_faults).
147165
/// @param context Prefix for error messages, e.g. "dem_chunks.init".
148166
/// @throws std::invalid_argument on the first violation.
149167
void validate(const std::string &context) const;
@@ -260,12 +278,18 @@ extended_dem dem_stitch_all(const std::vector<extended_dem> &dem_chunks);
260278
/// into one. The merged prior is computed from the individual priors using
261279
/// one of these two rules:
262280
///
263-
/// - or_combine (default): p_merged = 1 - prod_i(1 - p_i)
264-
/// Exact probability that at least one independent event fires.
265-
/// Use for physical fault mechanisms.
266-
///
267-
/// - sum_combine: p_merged = sum_i(p_i)
268-
/// Linear approximation valid when all p_i are small.
281+
/// - or_combine (default): p_merged = 1/2 * (1 - prod_i(1 - 2 p_i))
282+
/// Exact probability that an odd number of independent events fire, which
283+
/// is the net GF(2) effect of identical DEM columns (even counts cancel).
284+
/// Pairwise this is P(A xor B) = p + q - 2 p q, matching
285+
/// detector_error_model canonicalization. Prefer this for physical fault
286+
/// mechanisms.
287+
///
288+
/// - sum_combine: p_merged = min(1, sum_i(p_i))
289+
/// Linear approximation valid when all p_i are small. The sum of several
290+
/// larger priors can exceed 1, which is not a probability any decoder can
291+
/// use, so the result is clamped; prefer or_combine when the priors are
292+
/// not small.
269293
enum class prior_combine_mode { or_combine, sum_combine };
270294

271295
/// @brief Merge fault columns with identical row support into single columns.
@@ -424,7 +448,10 @@ dem_chunks_to_o_sparse(const std::vector<extended_dem> &dem_chunks);
424448
///
425449
/// Seams only have to contract pairwise, so a phase decomposition works: the
426450
/// first chunk may have no incoming seam (its interior carries round 0) and the
427-
/// last may have no outgoing one.
451+
/// last may have no outgoing one. As with dem_close(), the last chunk's
452+
/// out_syndrome is discarded: any detector that should appear in the closed
453+
/// DEM must already live in some chunk's in_syndrome or interior (for a
454+
/// dem_chunks_spec final phase, that means H_in_sparse / H_mid_sparse).
428455
///
429456
/// @param dem_chunks Non-empty sequence of chunks in round order. Each chunk's
430457
/// out_syndrome must match the next one's in_syndrome, and all
@@ -457,7 +484,15 @@ dem_chunks_to_pcm(const std::vector<extended_dem> &dem_chunks);
457484
/// - detector_error_matrix: [in_syndrome stacked above interior]
458485
/// - observables_flips_matrix: observables
459486
/// - error_rates: fault_priors
460-
/// - out_syndrome rows are dropped (no final data measurement assumed).
487+
///
488+
/// out_syndrome is intentionally dropped. Closing models a terminated
489+
/// experiment: there is no later round for the outgoing seam to differ against,
490+
/// matching dem_from_css_matrices (final-round faults touch only the last
491+
/// detector band). Put any detector that must survive closing into in_syndrome
492+
/// or interior instead — for example a final data-readout boundary belongs in
493+
/// the last chunk's in_syndrome / interior (dem_chunks.final.H_in_sparse /
494+
/// H_mid_sparse), never only in out_syndrome. dem_chunks_spec::validate()
495+
/// already rejects a nonempty final.H_out_sparse for this reason.
461496
///
462497
/// Invariant (up to canonicalization):
463498
/// dem_close(dem_stitch_all(T one-round chunks))

libs/qec/include/cudaq/qec/realtime/decoding_config.h

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ struct decoder_config {
9191
/// requires knowing the round count up front; these phases describe one
9292
/// round each so the round count can be chosen (or grown) at run time. See
9393
/// cudaq::qec::dem_chunks_from_spec() for expansion to a chunk sequence.
94+
///
95+
/// A configuration that also has a nonempty H_sparse is flat, and that
96+
/// matrix is the one decoders are built from -- the phases are then only a
97+
/// record of where it came from. Form selection keys off H_sparse.empty(),
98+
/// so an omitted H_sparse and an explicit empty list both count as chunk
99+
/// form. Nonempty H_sparse is exactly the state expand_dem_chunks() leaves
100+
/// behind, which is what lets an expanded configuration round-trip through
101+
/// YAML; it is not a way to override individual rounds.
94102
std::optional<dem_chunks_spec> dem_chunks;
95103
/// How many rounds to expand `dem_chunks` into. Required with dem_chunks and
96104
/// rejected without it. This is the round count the flat form would otherwise
@@ -190,8 +198,9 @@ class multi_decoder_config {
190198
/// therefore only has to understand the flat form.
191199
///
192200
/// Does nothing to a configuration that is already flat (one whose `H_sparse`
193-
/// is set, or which carries no `dem_chunks` at all), so it is safe to call
194-
/// unconditionally.
201+
/// is nonempty, or which carries no `dem_chunks` at all), so it is safe to
202+
/// call unconditionally. An empty H_sparse with dem_chunks present is still
203+
/// treated as chunk form.
195204
///
196205
/// @return The closed DEM the flat fields were derived from, so a caller that
197206
/// also wants its per-fault priors does not have to expand a second

libs/qec/lib/dem_construction.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ detector_error_model dem_from_css_matrices(const css_code_matrices &code,
2929
if (num_rounds == 0)
3030
throw std::invalid_argument("num_rounds must be >= 1");
3131

32+
// Checked before the n == 0 early-out below so that a malformed rate is
33+
// reported even when the code matrices describe no qubits to apply it to.
34+
validate_noise_rates(noise);
35+
3236
detector_error_model result;
3337
const std::size_t n = resolve_num_qubits(code);
3438

@@ -62,7 +66,7 @@ detector_error_model dem_from_css_matrices(const css_code_matrices &code,
6266
const std::size_t d = nz + nx; // detector rows per round = total checks
6367

6468
// pm_per_check must have length d (= nz + nx) when non-empty.
65-
check_per_qubit_size(noise.pm_per_check, d, "pm_per_check");
69+
check_rate_vector_size(noise.pm_per_check, d, "pm_per_check", "n_checks");
6670

6771
const std::size_t n_detectors = num_rounds * d;
6872
const std::size_t n_observables = kz + kx;

0 commit comments

Comments
 (0)