Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,6 @@ eclipse-*bin/
/.sass-cache
# User-specific .bazelrc
user.bazelrc

# Ignore python extension module produced by CMake.
src/tesseract_decoder*.so
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 7 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
25 changes: 13 additions & 12 deletions src/py/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -219,17 +219,18 @@ 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**:

```python
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("""
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions src/py/shared_decoding_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion src/py/simplex_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

import pytest
import numpy as np
import stim

from src import tesseract_decoder
Expand Down Expand Up @@ -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)

Expand Down
7 changes: 5 additions & 2 deletions src/py/tesseract_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

import pytest
import numpy as np
import stim

from src import tesseract_decoder
Expand Down Expand Up @@ -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)

Expand Down
51 changes: 36 additions & 15 deletions src/simplex.pybind.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<py::scoped_ostream_redirect, py::scoped_estream_redirect>(), R"pbdoc(
.def(
"decode_to_errors",
[](SimplexDecoder& self, const py::array_t<bool>& 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<uint64_t> 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<py::scoped_ostream_redirect, py::scoped_estream_redirect>(),
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<size_t>& predicted_errors) {
Expand Down Expand Up @@ -228,11 +250,10 @@ void add_simplex_module(py::module& root) {
"decode",
[](SimplexDecoder& self, const py::array_t<bool>& 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<uint64_t> detections;
Expand Down Expand Up @@ -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.
Expand Down
89 changes: 64 additions & 25 deletions src/tesseract.pybind.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<const std::vector<uint64_t>&>(&TesseractDecoder::decode_to_errors),
py::arg("detections"),
py::call_guard<py::scoped_ostream_redirect, py::scoped_estream_redirect>(), R"pbdoc(
.def(
"decode_to_errors",
[](TesseractDecoder& self, const py::array_t<bool>& syndrome) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it is wise to have logic in this part of the code ... having logic here should only happen for simple copying or conversion. lets either modify the C++ decode_to_errors method to have the checks and filter or create a new one that does with tests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a future refactor, let's add methods to the SimplexDecoder and TesseractDecoder APIs which accept a dense representation of the detection events. These will just convert to a sparse representation and call the sparse API methods. The checks for the array size can happen there instead of being duplicated throughout all the pybind glue code for both decoders. WDYT?

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<uint64_t> 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<py::scoped_ostream_redirect, py::scoped_estream_redirect>(),
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<const std::vector<uint64_t>&, size_t, size_t>(
&TesseractDecoder::decode_to_errors),
py::arg("detections"), py::arg("det_order"), py::arg("det_beam"),
py::call_guard<py::scoped_ostream_redirect, py::scoped_estream_redirect>(), R"pbdoc(
)pbdoc")
.def(
"decode_to_errors",
[](TesseractDecoder& self, const py::array_t<bool>& 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<uint64_t> 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<py::scoped_ostream_redirect, py::scoped_estream_redirect>(),
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
Expand All @@ -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<size_t>& predicted_errors) {
Expand Down Expand Up @@ -355,11 +395,10 @@ void add_tesseract_module(py::module& root) {
"decode",
[](TesseractDecoder& self, const py::array_t<bool>& 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<uint64_t> detections;
Expand Down Expand Up @@ -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.
Expand Down
Loading
Loading