diff --git a/.gitignore b/.gitignore index 5b5d20b..66d8f70 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ eclipse-*bin/ /.sass-cache # User-specific .bazelrc user.bazelrc + +# Ignore python extension module produced by CMake. +src/tesseract_decoder*.so diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ea68e20 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,5 @@ +# Agent Instructions + +- Use the **CMake** build system when interacting with this repository. Humans use Bazel. +- A bug in some LLM coding environments makes Bazel difficult to use, so agents should rely on CMake. +- Keep both the CMake and Bazel builds working at all times. diff --git a/CMakeLists.txt b/CMakeLists.txt index 33420c7..10e5ea6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -103,4 +103,11 @@ pybind11_add_module(tesseract_decoder MODULE ${TESSERACT_SRC_DIR}/tesseract.pybi target_compile_options(tesseract_decoder PRIVATE ${OPT_COPTS}) target_include_directories(tesseract_decoder PRIVATE ${TESSERACT_SRC_DIR}) target_link_libraries(tesseract_decoder PRIVATE common utils simplex tesseract_lib) +set_target_properties(tesseract_decoder PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/src + LIBRARY_OUTPUT_DIRECTORY_DEBUG ${PROJECT_SOURCE_DIR}/src + LIBRARY_OUTPUT_DIRECTORY_RELEASE ${PROJECT_SOURCE_DIR}/src + LIBRARY_OUTPUT_DIRECTORY_MINSIZEREL ${PROJECT_SOURCE_DIR}/src + LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO ${PROJECT_SOURCE_DIR}/src +) diff --git a/README.md b/README.md index 30230f7..2e19e43 100644 --- a/README.md +++ b/README.md @@ -190,15 +190,15 @@ config = tesseract.TesseractConfig(dem=dem, det_beam=50) # 3. Create a decoder instance decoder = config.compile_decoder() -# 4. Simulate detection events -syndrome = [0, 1, 1] +# 4. Simulate detector outcomes +syndrome = np.array([0, 1, 1], dtype=bool) # 5a. Decode to observables flipped_observables = decoder.decode(syndrome) print(f"Flipped observables: {flipped_observables}") # 5b. Alternatively, decode to errors -decoder.decode_to_errors(np.where(syndrome)[0]) +decoder.decode_to_errors(syndrome) predicted_errors = decoder.predicted_errors_buffer # Indices of predicted errors print(f"Predicted errors indices: {predicted_errors}") diff --git a/src/py/README.md b/src/py/README.md index 295fece..a7bc645 100644 --- a/src/py/README.md +++ b/src/py/README.md @@ -64,28 +64,28 @@ print(f"Custom configuration detection penalty: {config2.det_beam}") #### Class `tesseract.TesseractDecoder` This is the main class that implements the Tesseract decoding logic. * `TesseractDecoder(config: tesseract.TesseractConfig)` -* `decode_to_errors(detections: list[int])` -* `decode_to_errors(detections: list[int], det_order: int, det_beam: int)` +* `decode_to_errors(syndrome: np.ndarray)` +* `decode_to_errors(syndrome: np.ndarray, det_order: int, det_beam: int)` * `get_observables_from_errors(predicted_errors: list[int]) -> list[bool]` * `cost_from_errors(predicted_errors: list[int]) -> float` -* `decode(detections: list[int]) -> list[bool]` +* `decode(syndrome: np.ndarray) -> np.ndarray` Explanation of each method: -#### `decode_to_errors(detections: list[int])` +#### `decode_to_errors(syndrome: np.ndarray)` Decodes a single measurement shot to predict a list of errors. -* **Parameters:** `detections` is a list of integers that represent the indices of the detectors that have fired in a single shot. +* **Parameters:** `syndrome` is a 1D NumPy array of booleans representing the detector outcomes for a single shot. * **Returns:** A list of integers, where each integer is the index of a predicted error. -#### `decode_to_errors(detections: list[int], det_order: int, det_beam: int)` +#### `decode_to_errors(syndrome: np.ndarray, det_order: int, det_beam: int)` An overloaded version of the `decode_to_errors` method that allows for a different decoding strategy. * **Parameters:** - * `detections` is a list of integers representing the indices of the fired detectors. + * `syndrome` is a 1D NumPy array of booleans representing the detector outcomes for a single shot. * `det_order` is an integer that specifies a different ordering of detectors to use for the decoding. @@ -219,10 +219,10 @@ print(f"Configuration verbose enabled: {config.verbose}") This is the main class for performing decoding using the Simplex algorithm. * `SimplexDecoder(config: simplex.SimplexConfig)` * `init_ilp()` -* `decode_to_errors(detections: list[int])` +* `decode_to_errors(syndrome: np.ndarray)` * `get_observables_from_errors(predicted_errors: list[int]) -> list[bool]` * `cost_from_errors(predicted_errors: list[int]) -> float` -* `decode(detections: list[int]) -> list[bool]` +* `decode(syndrome: np.ndarray) -> np.ndarray` **Example Usage**: @@ -230,6 +230,7 @@ This is the main class for performing decoding using the Simplex algorithm. import tesseract_decoder.simplex as simplex import stim import tesseract_decoder.common as common +import numpy as np # Create a DEM and a configuration dem = stim.DetectorErrorModel(""" @@ -245,9 +246,9 @@ decoder = simplex.SimplexDecoder(config) decoder.init_ilp() # Decode a shot where detector D1 fired -detections = [1] -flipped_observables = decoder.decode(detections) -print(f"Flipped observables for detections {detections}: {flipped_observables}") +syndrome = np.array([0, 1], dtype=bool) +flipped_observables = decoder.decode(syndrome) +print(f"Flipped observables for syndrome {syndrome.tolist()}: {flipped_observables}") # Access predicted errors predicted_error_indices = decoder.predicted_errors_buffer diff --git a/src/py/shared_decoding_tests.py b/src/py/shared_decoding_tests.py index 3d1656b..9569ed5 100644 --- a/src/py/shared_decoding_tests.py +++ b/src/py/shared_decoding_tests.py @@ -316,16 +316,16 @@ def shared_test_merge_errors_affects_cost(decoder_class, config_class): error(0.01) D0 """ ) - detections = [0] + syndrome = np.array([True], dtype=bool) config_no_merge = config_class(dem, merge_errors=False) decoder_no_merge = decoder_class(config_no_merge) - predicted_errors_no_merge = decoder_no_merge.decode_to_errors(detections) + predicted_errors_no_merge = decoder_no_merge.decode_to_errors(syndrome) cost_no_merge = decoder_no_merge.cost_from_errors(decoder_no_merge.predicted_errors_buffer) config_merge = config_class(dem, merge_errors=True) decoder_merge = decoder_class(config_merge) - predicted_errors_merge = decoder_merge.decode_to_errors(detections) + predicted_errors_merge = decoder_merge.decode_to_errors(syndrome) cost_merge = decoder_merge.cost_from_errors(decoder_merge.predicted_errors_buffer) p_merged = 0.1 * (1 - 0.01) + 0.01 * (1 - 0.1) diff --git a/src/py/simplex_test.py b/src/py/simplex_test.py index 3a228d9..752f9e8 100644 --- a/src/py/simplex_test.py +++ b/src/py/simplex_test.py @@ -13,6 +13,7 @@ # limitations under the License. import pytest +import numpy as np import stim from src import tesseract_decoder @@ -56,7 +57,7 @@ def test_create_simplex_decoder(): decoder = tesseract_decoder.simplex.SimplexDecoder( tesseract_decoder.simplex.SimplexConfig(_DETECTOR_ERROR_MODEL, window_length=5) ) - decoder.decode_to_errors([1]) + decoder.decode_to_errors(np.array([False, True], dtype=bool)) assert decoder.get_observables_from_errors([1]) == [] assert decoder.cost_from_errors([2]) == pytest.approx(1.0986123) diff --git a/src/py/tesseract_test.py b/src/py/tesseract_test.py index ac40f11..4355497 100644 --- a/src/py/tesseract_test.py +++ b/src/py/tesseract_test.py @@ -13,6 +13,7 @@ # limitations under the License. import pytest +import numpy as np import stim from src import tesseract_decoder @@ -191,8 +192,10 @@ def test_create_tesseract_config_no_dem_with_custom_args(): def test_create_tesseract_decoder(): config = tesseract_decoder.tesseract.TesseractConfig(_DETECTOR_ERROR_MODEL) decoder = tesseract_decoder.tesseract.TesseractDecoder(config) - decoder.decode_to_errors([0]) - decoder.decode_to_errors(detections=[0], det_order=0, det_beam=0) + decoder.decode_to_errors(np.array([True, False], dtype=bool)) + decoder.decode_to_errors( + syndrome=np.array([True, False], dtype=bool), det_order=0, det_beam=0 + ) assert decoder.get_observables_from_errors([1]) == [] assert decoder.cost_from_errors([1]) == pytest.approx(0.5108256237659907) diff --git a/src/simplex.pybind.h b/src/simplex.pybind.h index 79c8d59..9d00d3c 100644 --- a/src/simplex.pybind.h +++ b/src/simplex.pybind.h @@ -140,20 +140,42 @@ void add_simplex_module(py::module& root) { This method must be called before decoding. )pbdoc") - .def("decode_to_errors", &SimplexDecoder::decode_to_errors, py::arg("detections"), - py::call_guard(), R"pbdoc( + .def( + "decode_to_errors", + [](SimplexDecoder& self, const py::array_t& syndrome) { + if ((size_t)syndrome.size() != self.num_detectors) { + std::string msg = "Syndrome array size (" + std::to_string(syndrome.size()) + + ") does not match the number of detectors in the decoder (" + + std::to_string(self.num_detectors) + ")."; + throw std::invalid_argument(msg); + } + + std::vector detections; + auto syndrome_unchecked = syndrome.unchecked<1>(); + for (size_t i = 0; i < (size_t)syndrome_unchecked.size(); ++i) { + if (syndrome_unchecked(i)) { + detections.push_back(i); + } + } + self.decode_to_errors(detections); + return self.predicted_errors_buffer; + }, + py::arg("syndrome"), + py::call_guard(), + R"pbdoc( Decodes a single shot to a list of error indices. Parameters ---------- - detections : list[int] - A list of indices of the detectors that have fired. + syndrome : np.ndarray + A 1D NumPy array of booleans representing the detector outcomes for a single shot. + The length of the array should match the number of detectors in the DEM. Returns ------- list[int] A list of predicted error indices. - )pbdoc") + )pbdoc") .def( "get_observables_from_errors", [](SimplexDecoder& self, const std::vector& predicted_errors) { @@ -228,11 +250,10 @@ void add_simplex_module(py::module& root) { "decode", [](SimplexDecoder& self, const py::array_t& syndrome) { if ((size_t)syndrome.size() != self.num_detectors) { - std::ostringstream msg; - msg << "Syndrome array size (" << syndrome.size() - << ") does not match the number of detectors in the decoder (" - << self.num_detectors << ")."; - throw std::invalid_argument(msg.str()); + std::string msg = "Syndrome array size (" + std::to_string(syndrome.size()) + + ") does not match the number of detectors in the decoder (" + + std::to_string(self.num_detectors) + ")."; + throw std::invalid_argument(msg); } std::vector detections; @@ -287,11 +308,11 @@ void add_simplex_module(py::module& root) { size_t num_detectors = syndromes_unchecked.shape(1); if (num_detectors != self.num_detectors) { - std::ostringstream msg; - msg << "The number of detectors in the input array (" << num_detectors - << ") does not match the number of detectors in the decoder (" - << self.num_detectors << ")."; - throw std::invalid_argument(msg.str()); + std::string msg = "The number of detectors in the input array (" + + std::to_string(num_detectors) + + ") does not match the number of detectors in the decoder (" + + std::to_string(self.num_detectors) + ")."; + throw std::invalid_argument(msg); } // Allocate the result array. diff --git a/src/tesseract.pybind.h b/src/tesseract.pybind.h index 0724b60..b94c27f 100644 --- a/src/tesseract.pybind.h +++ b/src/tesseract.pybind.h @@ -243,33 +243,73 @@ void add_tesseract_module(py::module& root) { config : TesseractConfig The configuration object for the decoder. )pbdoc") - .def("decode_to_errors", - py::overload_cast&>(&TesseractDecoder::decode_to_errors), - py::arg("detections"), - py::call_guard(), R"pbdoc( + .def( + "decode_to_errors", + [](TesseractDecoder& self, const py::array_t& syndrome) { + if ((size_t)syndrome.size() != self.num_detectors) { + std::string msg = "Syndrome array size (" + std::to_string(syndrome.size()) + + ") does not match the number of detectors in the decoder (" + + std::to_string(self.num_detectors) + ")."; + throw std::invalid_argument(msg); + } + + std::vector detections; + auto syndrome_unchecked = syndrome.unchecked<1>(); + for (size_t i = 0; i < (size_t)syndrome_unchecked.size(); ++i) { + if (syndrome_unchecked(i)) { + detections.push_back(i); + } + } + self.decode_to_errors(detections); + return self.predicted_errors_buffer; + }, + py::arg("syndrome"), + py::call_guard(), + R"pbdoc( Decodes a single shot to a list of error indices. Parameters ---------- - detections : list[int] - A list of indices of the detectors that have fired. + syndrome : np.ndarray + A 1D NumPy array of booleans representing the detector outcomes for a single shot. + The length of the array should match the number of detectors in the DEM. Returns ------- list[int] A list of predicted error indices. - )pbdoc") - .def("decode_to_errors", - py::overload_cast&, size_t, size_t>( - &TesseractDecoder::decode_to_errors), - py::arg("detections"), py::arg("det_order"), py::arg("det_beam"), - py::call_guard(), R"pbdoc( + )pbdoc") + .def( + "decode_to_errors", + [](TesseractDecoder& self, const py::array_t& syndrome, size_t det_order, + size_t det_beam) { + if ((size_t)syndrome.size() != self.num_detectors) { + std::string msg = "Syndrome array size (" + std::to_string(syndrome.size()) + + ") does not match the number of detectors in the decoder (" + + std::to_string(self.num_detectors) + ")."; + throw std::invalid_argument(msg); + } + + std::vector detections; + auto syndrome_unchecked = syndrome.unchecked<1>(); + for (size_t i = 0; i < (size_t)syndrome_unchecked.size(); ++i) { + if (syndrome_unchecked(i)) { + detections.push_back(i); + } + } + self.decode_to_errors(detections, det_order, det_beam); + return self.predicted_errors_buffer; + }, + py::arg("syndrome"), py::arg("det_order"), py::arg("det_beam"), + py::call_guard(), + R"pbdoc( Decodes a single shot using a specific detector ordering and beam size. Parameters ---------- - detections : list[int] - A list of indices of the detectors that have fired. + syndrome : np.ndarray + A 1D NumPy array of booleans representing the detector outcomes for a single shot. + The length of the array should match the number of detectors in the DEM. det_order : int The index of the detector ordering to use. det_beam : int @@ -279,7 +319,7 @@ void add_tesseract_module(py::module& root) { ------- list[int] A list of predicted error indices. - )pbdoc") + )pbdoc") .def( "get_observables_from_errors", [](TesseractDecoder& self, const std::vector& predicted_errors) { @@ -355,11 +395,10 @@ void add_tesseract_module(py::module& root) { "decode", [](TesseractDecoder& self, const py::array_t& syndrome) { if ((size_t)syndrome.size() != self.num_detectors) { - std::ostringstream msg; - msg << "Syndrome array size (" << syndrome.size() - << ") does not match the number of detectors in the decoder (" - << self.num_detectors << ")."; - throw std::invalid_argument(msg.str()); + std::string msg = "Syndrome array size (" + std::to_string(syndrome.size()) + + ") does not match the number of detectors in the decoder (" + + std::to_string(self.num_detectors) + ")."; + throw std::invalid_argument(msg); } std::vector detections; @@ -413,11 +452,11 @@ void add_tesseract_module(py::module& root) { size_t num_detectors = syndromes_unchecked.shape(1); if (num_detectors != self.num_detectors) { - std::ostringstream msg; - msg << "The number of detectors in the input array (" << num_detectors - << ") does not match the number of detectors in the decoder (" - << self.num_detectors << ")."; - throw std::invalid_argument(msg.str()); + std::string msg = "The number of detectors in the input array (" + + std::to_string(num_detectors) + + ") does not match the number of detectors in the decoder (" + + std::to_string(self.num_detectors) + ")."; + throw std::invalid_argument(msg); } // Allocate the result array. diff --git a/src/tesseract_sinter_compat.pybind.h b/src/tesseract_sinter_compat.pybind.h index 3d15e6b..623253d 100644 --- a/src/tesseract_sinter_compat.pybind.h +++ b/src/tesseract_sinter_compat.pybind.h @@ -83,7 +83,7 @@ struct TesseractSinterCompiledDecoder { // 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); - for (int obs_index : predictions) { + for (size_t obs_index : predictions) { if (obs_index >= 0 && obs_index < num_observables) { single_result_buffer[obs_index / 8] ^= (1 << (obs_index % 8)); } @@ -191,7 +191,7 @@ struct TesseractSinterDecoder { // Pack the predictions back into a bit-packed format. std::fill(single_result_data.begin(), single_result_data.end(), 0); - for (int obs_index : predictions) { + for (size_t obs_index : predictions) { if (obs_index >= 0 && obs_index < num_obs) { single_result_data[obs_index / 8] ^= (1 << (obs_index % 8)); }