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..02d92408 100644 --- a/src/py/tesseract_sinter_compat_test.py +++ b/src/py/tesseract_sinter_compat_test.py @@ -21,6 +21,7 @@ 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) @@ -104,7 +105,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 +150,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 +163,59 @@ 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 + """) + + sampler = StimThenDecodeSampler( + decoder=TesseractSinterDecoder(), + count_observable_error_combos=False, + count_detection_events=False, + tmp_dir=None, + ).compiled_sampler_for_task( + sinter.Task(circuit=circuit, detector_error_model=dem) + ) + result = sampler.sample(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 diff --git a/src/tesseract_sinter_compat.pybind.h b/src/tesseract_sinter_compat.pybind.h index aecaa6b8..0ee8eb19 100644 --- a/src/tesseract_sinter_compat.pybind.h +++ b/src/tesseract_sinter_compat.pybind.h @@ -56,10 +56,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(); @@ -81,13 +82,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. @@ -321,8 +323,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")