Skip to content

Commit b53bdce

Browse files
committed
update sliding window tests to cope with same-H diff-O
Signed-off-by: Kaiqi Yan <kaiqiy@nvidia.com>
1 parent c374d76 commit b53bdce

2 files changed

Lines changed: 93 additions & 14 deletions

File tree

libs/qec/lib/detector_error_model.cpp

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ void detector_error_model::canonicalize_for_rounds(
9696
"or the detector_error_matrix was computed incorrectly.");
9797
}
9898

99+
// Cap the number of "same syndrome, different observable" warnings emitted
100+
// per invocation. Short-distance codes can have many such mechanisms, and
101+
// logging every one of them would spam the console.
102+
constexpr std::size_t max_same_syndrome_diff_obs_warnings = 10;
103+
std::size_t num_same_syndrome_diff_obs = 0;
104+
99105
for (std::size_t c = 0; c < num_cols; c++) {
100106
auto column_index = column_order[c];
101107
auto &curr_row_indices = row_indices[column_index];
@@ -140,12 +146,14 @@ void detector_error_model::canonicalize_for_rounds(
140146
// Either the syndrome differs, or the same syndrome has a different
141147
// observable flip. In both cases this is a distinct error mechanism.
142148
if (prev_row_indices == curr_row_indices) {
143-
cudaq::warn(
144-
"detector_error_model::canonicalize_for_rounds: identical "
145-
"syndromes exist in detector_error_matrix but have different "
146-
"observables in observables_flips_matrix; keeping column {} as a "
147-
"distinct error mechanism (previous column {})",
148-
column_index, previous_column);
149+
if (num_same_syndrome_diff_obs < max_same_syndrome_diff_obs_warnings)
150+
cudaq::warn(
151+
"detector_error_model::canonicalize_for_rounds: identical "
152+
"syndromes exist in detector_error_matrix but have different "
153+
"observables in observables_flips_matrix; keeping column {} as "
154+
"a distinct error mechanism (previous column {})",
155+
column_index, previous_column);
156+
num_same_syndrome_diff_obs++;
149157
}
150158
new_row_indices.push_back(curr_row_indices);
151159
new_weights.push_back(error_rates[column_index]);
@@ -156,6 +164,16 @@ void detector_error_model::canonicalize_for_rounds(
156164
}
157165
}
158166

167+
// Emit a single summary if we suppressed any per-column warnings above.
168+
if (num_same_syndrome_diff_obs > max_same_syndrome_diff_obs_warnings)
169+
cudaq::warn(
170+
"detector_error_model::canonicalize_for_rounds: found {} columns with "
171+
"identical syndromes but different observables; suppressed {} "
172+
"additional warnings (only the first {} were shown).",
173+
num_same_syndrome_diff_obs,
174+
num_same_syndrome_diff_obs - max_same_syndrome_diff_obs_warnings,
175+
max_same_syndrome_diff_obs_warnings);
176+
159177
std::swap(this->error_rates, new_weights);
160178
if (has_error_ids)
161179
std::swap(*this->error_ids, new_error_ids);

libs/qec/python/tests/test_sliding_window.py

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,20 @@ def test_sliding_window_1(decoder_name, batched, num_rounds, num_windows):
4646
col = np.random.randint(0, dem.detector_error_matrix.shape[1])
4747
syndromes[shot, :] = dem.detector_error_matrix[:, col].T
4848

49-
# First compare the results of the full decoder to the sliding window
50-
# decoder using an inner decoder of the full window size. The results should
51-
# be the same.
52-
full_decoder = qec.get_decoder(decoder_name, dem.detector_error_matrix)
49+
# Compare the full decoder against the sliding window decoder (configured as
50+
# a single full-size window). For column-space decoders (single_error_lut)
51+
# the corrections must be identical; for matching decoders (pymatching),
52+
# same-H/different-O degeneracy makes the exact error column non-unique, so
53+
# we instead require each correction to reproduce the input syndrome.
54+
if decoder_name == "pymatching":
55+
# canonicalize_for_rounds keeps same-syndrome/different-observable
56+
# columns distinct; these are parallel edges in the matching graph that
57+
# PyMatching's default 'disallow' strategy rejects, so merge them.
58+
full_decoder = qec.get_decoder(decoder_name,
59+
dem.detector_error_matrix,
60+
merge_strategy="independent")
61+
else:
62+
full_decoder = qec.get_decoder(decoder_name, dem.detector_error_matrix)
5363
num_syndromes_per_round = dem.detector_error_matrix.shape[0] // num_rounds
5464

5565
sw_as_full_decoder = qec.get_decoder(
@@ -67,18 +77,69 @@ def test_sliding_window_1(decoder_name, batched, num_rounds, num_windows):
6777
'merge_strategy': 'smallest_weight'
6878
})
6979

80+
# H maps an error (column space) to the syndrome it produces (mod 2).
81+
H = np.asarray(dem.detector_error_matrix, dtype=np.int64)
82+
# pymatching is a graph/matching decoder: for degenerate same-H/different-O
83+
# groups it cannot return a unique column, so validate by syndrome
84+
# reproduction rather than exact column equality.
85+
check_syndrome_only = decoder_name == "pymatching"
86+
7087
if batched:
7188
full_results = full_decoder.decode_batch(syndromes)
7289
sw_results = sw_as_full_decoder.decode_batch(syndromes)
73-
num_mismatches = np.count_nonzero(
74-
np.any(full_results.result != sw_results.result, axis=1))
75-
assert num_mismatches == 0
90+
if check_syndrome_only:
91+
full_e = np.asarray(full_results.result, dtype=np.int64)
92+
sw_e = np.asarray(sw_results.result, dtype=np.int64)
93+
target = syndromes.astype(np.int64)
94+
# ASSERT: every correction (full and windowed) explains the observed
95+
# syndrome, i.e. (H @ e) % 2 == syndrome for all shots.
96+
assert np.array_equal((full_e @ H.T) % 2, target)
97+
assert np.array_equal((sw_e @ H.T) % 2, target)
98+
else:
99+
# ASSERT: column-space decoders produce identical corrections.
100+
num_mismatches = np.count_nonzero(
101+
np.any(full_results.result != sw_results.result, axis=1))
102+
assert num_mismatches == 0
76103

77104
else:
78105
num_mismatches = 0
79106
for syndrome in syndromes:
80107
r1 = full_decoder.decode(syndrome)
81108
r2 = sw_as_full_decoder.decode(syndrome)
82-
if not np.array_equal(r1.result, r2.result):
109+
if check_syndrome_only:
110+
# A correction is valid if it reproduces the observed syndrome
111+
# via H (mod 2); count a mismatch if either decoder fails to.
112+
target = np.asarray(syndrome, dtype=np.int64)
113+
e1 = np.asarray(r1.result, dtype=np.int64)
114+
e2 = np.asarray(r2.result, dtype=np.int64)
115+
if not (np.array_equal((H @ e1) % 2, target) and np.array_equal(
116+
(H @ e2) % 2, target)):
117+
num_mismatches += 1
118+
elif not np.array_equal(r1.result, r2.result):
83119
num_mismatches += 1
84120
assert num_mismatches == 0
121+
122+
123+
def test_pymatching_parallel_edges_use_observable_faults():
124+
# Same detector syndrome with different observable flips represents
125+
# distinct logical fault mechanisms. H-only PyMatching cannot distinguish
126+
# them, so the observable-aware path must preserve O and merge parallel
127+
# graph edges inside PyMatching rather than dropping DEM columns.
128+
H = np.array([[1, 1]], dtype=np.uint8)
129+
O = np.array([[1, 0], [0, 1]], dtype=np.uint8)
130+
error_rates = np.array([0.1, 0.2], dtype=np.float64)
131+
132+
with pytest.raises(ValueError, match="Parallel edges not permitted"):
133+
qec.get_decoder("pymatching", H)
134+
135+
decoder = qec.get_decoder("pymatching",
136+
H,
137+
O=O,
138+
error_rate_vec=error_rates,
139+
merge_strategy="independent")
140+
result = decoder.decode_batch(np.array([[1]], dtype=np.uint8))
141+
142+
assert isinstance(result, qec.BatchDecoderResult)
143+
assert result.result.shape[0] == 1
144+
assert result.result.shape[1] > 0
145+
assert result.converged.tolist() == [True]

0 commit comments

Comments
 (0)