diff --git a/README.md b/README.md index 15331c43..c3583e6f 100644 --- a/README.md +++ b/README.md @@ -367,6 +367,9 @@ sinter collect \ Sinter efficiently manages the execution of these tasks, and Tesseract is used for decoding. For more usage examples, see the tests in `src/py/tesseract_sinter_compat_test.py`. +The compiled Sinter integration reports low-confidence decoding outcomes as discards. Use +`errors + discards` when calculating a conservative logical failure count. + ## Good Starting Points for Tesseract Configurations: The [Tesseract paper](https://arxiv.org/pdf/2503.10988) recommends two setup for starting your exploration with tesseract: diff --git a/src/py/README.md b/src/py/README.md index 658932a8..729ea799 100644 --- a/src/py/README.md +++ b/src/py/README.md @@ -508,6 +508,9 @@ print(estimated_dem) ### Sinter Integration The Tesseract Python interface is compatible with the Sinter framework, which is a powerful tool for large-scale decoding, benchmarking, and error-rate estimation. +The compiled Sinter integration reports low-confidence decoding outcomes as discards. Use +`errors + discards` when calculating a conservative logical failure count. + #### The TesseractSinterDecoder Object All Sinter examples rely on this utility function to provide the Sinter-compatible Tesseract decoder. The default decoder dictionary also includes sparsified variants: diff --git a/src/py/tesseract_sinter_compat_test.py b/src/py/tesseract_sinter_compat_test.py index 1fc3413c..58ba6175 100644 --- a/src/py/tesseract_sinter_compat_test.py +++ b/src/py/tesseract_sinter_compat_test.py @@ -20,11 +20,32 @@ import sinter import stim import tesseract_decoder -from sinter._decoding._decoding import sample_decode +from sinter._decoding._stim_then_decode_sampler import StimThenDecodeSampler from tesseract_decoder import (TesseractSinterDecoder, make_tesseract_sinter_decoders_dict) +def sample_decode_with_discards( + *, + circuit, + num_shots, + dem=None, + decoder="tesseract", + count_detection_events=False, +): + if dem is None: + dem = circuit.detector_error_model(decompose_errors=True) + sampler = StimThenDecodeSampler( + decoder=make_tesseract_sinter_decoders_dict()[decoder], + count_observable_error_combos=False, + count_detection_events=count_detection_events, + tmp_dir=None, + ).compiled_sampler_for_task( + sinter.Task(circuit=circuit, detector_error_model=dem) + ) + return sampler.sample(num_shots) + + def test_tesseract_sinter_obj_exists(): """ Sanity check to ensure the decoder object exists and has the required methods. @@ -104,7 +125,7 @@ def test_decode_shots_bit_packed(): # Extract the expected predictions from the DEM expected_predictions = np.zeros( - (num_shots, (dem.num_observables + 7) // 8), dtype=np.uint8 + (num_shots, (dem.num_observables + 7) // 8 + 1), dtype=np.uint8 ) expected_predictions[0][0] |= 1 << 0 # Logical observable L0 is flipped @@ -149,7 +170,7 @@ def test_decode_shots_bit_packed_multi_shot(): ) expected_predictions = np.zeros( - (num_shots, (dem.num_observables + 7) // 8), dtype=np.uint8 + (num_shots, (dem.num_observables + 7) // 8 + 1), dtype=np.uint8 ) # Expected flip for shot 0 is L0 expected_predictions[0][0] |= 1 << 0 @@ -162,6 +183,55 @@ def test_decode_shots_bit_packed_multi_shot(): assert np.array_equal(predictions, expected_predictions) +def test_decode_shots_bit_packed_marks_low_confidence_shots_for_discard(): + dem = stim.DetectorErrorModel(""" + error(0.1) D0 L0 + detector(0, 0, 0) D0 + detector(0, 0, 1) D1 + """) + compiled_decoder = TesseractSinterDecoder().compile_decoder_for_dem(dem=dem) + detections = np.array([[0b01], [0b10]], dtype=np.uint8) + + predictions = compiled_decoder.decode_shots_bit_packed( + bit_packed_detection_event_data=detections + ) + + expected_predictions = np.array( + [ + [0b1, 0], + [0, 1], + ], + dtype=np.uint8, + ) + assert np.array_equal(predictions, expected_predictions) + + +def test_sinter_discards_low_confidence_shots(): + circuit = stim.Circuit(""" + R 0 1 + X_ERROR(1) 0 + M 0 1 + DETECTOR rec[-2] + DETECTOR rec[-1] + OBSERVABLE_INCLUDE(0) rec[-2] + """) + dem = stim.DetectorErrorModel(""" + error(0.1) D1 L0 + detector D0 + detector D1 + """) + + result = sample_decode_with_discards( + circuit=circuit, + dem=dem, + num_shots=5, + ) + + assert result.shots == 5 + assert result.discards == 5 + assert result.errors == 0 + + def test_decode_via_files_sanity_check(): """ Tests the 'decode_via_files' method by simulating a small circuit and @@ -383,14 +453,9 @@ def test_sinter_decode_repetition_code(): after_clifford_depolarization=0.05, ) - result = sample_decode( - circuit_obj=circuit, - circuit_path=None, - dem_obj=circuit.detector_error_model(decompose_errors=True), - dem_path=None, + result = sample_decode_with_discards( + circuit=circuit, num_shots=1000, - decoder="tesseract", - custom_decoders=make_tesseract_sinter_decoders_dict(), ) assert result.discards == 0 assert 0 <= result.errors <= 100 @@ -407,14 +472,9 @@ def test_sinter_decode_surface_code(): rounds=15, after_clifford_depolarization=0.001, ) - result = sample_decode( + result = sample_decode_with_discards( + circuit=circuit, num_shots=1000, - circuit_obj=circuit, - circuit_path=None, - dem_obj=circuit.detector_error_model(decompose_errors=True), - dem_path=None, - decoder="tesseract", - custom_decoders=make_tesseract_sinter_decoders_dict(), ) assert result.discards == 0 assert 0 <= result.errors <= 50 @@ -426,14 +486,9 @@ def test_sinter_empty(): Tests the 'tesseract' decoder on an empty circuit. """ circuit = stim.Circuit() - result = sample_decode( - circuit_obj=circuit, - circuit_path=None, - dem_obj=circuit.detector_error_model(decompose_errors=True), - dem_path=None, + result = sample_decode_with_discards( + circuit=circuit, num_shots=1000, - decoder="tesseract", - custom_decoders=make_tesseract_sinter_decoders_dict(), ) assert result.discards == 0 assert result.shots == 1000 @@ -449,14 +504,9 @@ def test_sinter_no_observables(): M 0 DETECTOR rec[-1] """) - result = sample_decode( - circuit_obj=circuit, - circuit_path=None, - dem_obj=circuit.detector_error_model(decompose_errors=True), - dem_path=None, + result = sample_decode_with_discards( + circuit=circuit, num_shots=1000, - decoder="tesseract", - custom_decoders=make_tesseract_sinter_decoders_dict(), ) assert result.discards == 0 assert result.shots == 1000 @@ -473,14 +523,9 @@ def test_sinter_invincible_observables(): DETECTOR rec[-2] OBSERVABLE_INCLUDE(1) rec[-1] """) - result = sample_decode( - circuit_obj=circuit, - circuit_path=None, - dem_obj=circuit.detector_error_model(decompose_errors=True), - dem_path=None, + result = sample_decode_with_discards( + circuit=circuit, num_shots=1000, - decoder="tesseract", - custom_decoders=make_tesseract_sinter_decoders_dict(), ) assert result.discards == 0 assert result.shots == 1000 @@ -500,16 +545,10 @@ def test_sinter_detector_counting(): OBSERVABLE_INCLUDE(0) rec[-1] OBSERVABLE_INCLUDE(1) rec[-1] rec[-2] """) - result = sample_decode( - circuit_obj=circuit, - circuit_path=None, - dem_obj=circuit.detector_error_model(decompose_errors=True), - dem_path=None, - post_mask=None, + result = sample_decode_with_discards( + circuit=circuit, num_shots=10000, - decoder="tesseract", count_detection_events=True, - custom_decoders=make_tesseract_sinter_decoders_dict(), ) assert result.discards == 0 assert result.custom_counts["detectors_checked"] == 20000 @@ -813,14 +852,10 @@ def test_sinter_decode_with_sparsify_decoders(decoder_name): after_clifford_depolarization=0.01, ) - result = sample_decode( - circuit_obj=circuit, - circuit_path=None, - dem_obj=circuit.detector_error_model(decompose_errors=True), - dem_path=None, + result = sample_decode_with_discards( + circuit=circuit, num_shots=100, decoder=decoder_name, - custom_decoders=make_tesseract_sinter_decoders_dict(), ) assert result.discards == 0 assert result.shots == 100 diff --git a/src/tesseract_sinter_compat.pybind.h b/src/tesseract_sinter_compat.pybind.h index f118750b..97f7fd31 100644 --- a/src/tesseract_sinter_compat.pybind.h +++ b/src/tesseract_sinter_compat.pybind.h @@ -58,10 +58,11 @@ struct TesseractSinterCompiledDecoder { const size_t num_shots = bit_packed_detection_event_data.shape(0); const uint64_t num_observable_bytes = (num_observables + 7) / 8; + const uint64_t num_result_bytes = num_observable_bytes + 1; - // Result buffer to store the predicted observables for all shots. + // Sinter interprets a nonzero trailing byte as a discard flag for the shot. auto result_array = - py::array_t({(py::ssize_t)num_shots, (py::ssize_t)num_observable_bytes}); + py::array_t({(py::ssize_t)num_shots, (py::ssize_t)num_result_bytes}); auto result_buffer = result_array.mutable_data(); const uint8_t* detections_data = bit_packed_detection_event_data.data(); @@ -83,13 +84,14 @@ struct TesseractSinterCompiledDecoder { std::vector predictions = decoder->decode(detections); // Store predictions into the output buffer - uint8_t* single_result_buffer = result_buffer + shot * num_observable_bytes; - std::fill(single_result_buffer, single_result_buffer + num_observable_bytes, 0); + uint8_t* single_result_buffer = result_buffer + shot * num_result_bytes; + std::fill(single_result_buffer, single_result_buffer + num_result_bytes, 0); for (size_t obs_index : predictions) { if (obs_index >= 0 && obs_index < num_observables) { single_result_buffer[obs_index / 8] ^= (1 << (obs_index % 8)); } } + single_result_buffer[num_observable_bytes] = decoder->low_confidence_flag; } // Return the result. @@ -323,8 +325,10 @@ void pybind_sinter_compat(py::module& root) { `(num_shots, ceil(num_detectors / 8))`. Each byte contains 8 bits of detection event data. A `1` in bit `k` of byte `j` indicates that detector `8j + k` fired. - :return: A 2D numpy array of shape `(num_shots, ceil(num_observables / 8))` - containing the predicted observable flips in a bit-packed format. + :return: A 2D numpy array of shape + `(num_shots, ceil(num_observables / 8) + 1)`. The first bytes contain + predicted observable flips in bit-packed format. The final byte is nonzero + when Sinter should discard the shot because decoding had low confidence. )pbdoc") .def_readwrite("num_detectors", &TesseractSinterCompiledDecoder::num_detectors, R"pbdoc(The number of detectors in the decoder's underlying DEM.)pbdoc")