@@ -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