From 72750f89f318efda6a6ef39880255385cfd08d17 Mon Sep 17 00:00:00 2001 From: Dragana Grbic Date: Sat, 9 Aug 2025 17:29:55 -0700 Subject: [PATCH 01/14] Python interface for Sinter API --- src/BUILD | 8 + src/py/BUILD | 11 + src/py/shared_decoding_tests.py | 14 ++ src/py/tesseract_sinter_compat_test.py | 315 +++++++++++++++++++++++++ src/tesseract_sinter_compat.pybind.cpp | 295 +++++++++++++++++++++++ 5 files changed, 643 insertions(+) create mode 100644 src/py/tesseract_sinter_compat_test.py create mode 100644 src/tesseract_sinter_compat.pybind.cpp diff --git a/src/BUILD b/src/BUILD index e92a6146..0556a2c9 100644 --- a/src/BUILD +++ b/src/BUILD @@ -90,12 +90,20 @@ pybind_extension( ], ) +pybind_extension( + name = "tesseract_sinter_compat", + srcs = ["tesseract_sinter_compat.pybind.cpp"], + deps = [ + ":libtesseract", + ], +) py_library( name="lib_tesseract_decoder", imports=["src"], deps=[ ":tesseract_decoder", + ":tesseract_sinter_compat", ], ) diff --git a/src/py/BUILD b/src/py/BUILD index 91e8528c..a4946c6f 100644 --- a/src/py/BUILD +++ b/src/py/BUILD @@ -58,6 +58,17 @@ py_test( ":shared_decoding_tests", ], ) +py_test( + name = "tesseract_sinter_compat_test", + srcs = ["tesseract_sinter_compat_test.py"], + visibility = ["//:__subpackages__"], + deps = [ + "@pypi//pytest", + "@pypi//stim", + "//src:lib_tesseract_decoder", + ], +) + compile_pip_requirements( name = "requirements", diff --git a/src/py/shared_decoding_tests.py b/src/py/shared_decoding_tests.py index f6697a98..948519a5 100644 --- a/src/py/shared_decoding_tests.py +++ b/src/py/shared_decoding_tests.py @@ -1,3 +1,17 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import pytest import numpy as np import stim diff --git a/src/py/tesseract_sinter_compat_test.py b/src/py/tesseract_sinter_compat_test.py new file mode 100644 index 00000000..a126d890 --- /dev/null +++ b/src/py/tesseract_sinter_compat_test.py @@ -0,0 +1,315 @@ +import pathlib +import pytest +import numpy as np +import stim +import shutil + +from src import tesseract_sinter_compat as tesseract_module + + +def test_tesseract_sinter_obj_exists(): + """ + Sanity check to ensure the decoder object exists and has the required methods. + """ + + decoder = tesseract_module.TesseractSinterDecoder() + assert hasattr(decoder, 'compile_decoder_for_dem') + assert hasattr(decoder, 'decode_via_files') + +def test_compile_decoder_for_dem(): + """ + Test the 'compile_decoder_for_dem' method with a specific DEM. + """ + + dem = stim.DetectorErrorModel(""" + detector(0, 0, 0) D0 + detector(0, 0, 1) D1 + detector(0, 0, 2) D2 + detector(0, 0, 3) D3 + error(0.1) D0 D1 L0 + error(0.1) D1 D2 L1 + error(0.1) D2 D3 L0 + """) + + decoder = tesseract_module.TesseractSinterDecoder() + compiled_decoder = decoder.compile_decoder_for_dem(dem=dem) + + assert compiled_decoder is not None + assert hasattr(compiled_decoder, 'decode_shots_bit_packed') + + # Verify the detector and observable counts are correct + assert compiled_decoder.num_detectors == dem.num_detectors + assert compiled_decoder.num_observables == dem.num_observables + +def test_decode_shots_bit_packed(): + """ + Tests the 'decode_shots_bit_packed' method with a specific DEM and detection event. + """ + + dem = stim.DetectorErrorModel(""" + detector(0, 0, 0) D0 + detector(0, 0, 1) D1 + detector(0, 0, 2) D2 + error(0.1) D0 D1 L0 + error(0.1) D1 D2 L1 + """) + + decoder = tesseract_module.TesseractSinterDecoder() + compiled_decoder = decoder.compile_decoder_for_dem(dem=dem) + + num_shots = 1 + detections_array = np.zeros((num_shots, (dem.num_detectors + 7) // 8), dtype=np.uint8) + + # Set bits for detectors D0 and D1 + # This should cause a logical flip on L0. + detections_array[0][0] |= (1 << 0) # D0 + detections_array[0][0] |= (1 << 1) # D1 + + predictions = compiled_decoder.decode_shots_bit_packed(bit_packed_detection_event_data=detections_array) + + # Extract the expected predictions from the DEM + expected_predictions = np.zeros((num_shots, (dem.num_observables + 7) // 8), dtype=np.uint8) + expected_predictions[0][0] |= (1 << 0) # Logical observable L0 is flipped + + # Compare the results + assert np.array_equal(predictions, expected_predictions) + + + +def test_decode_shots_bit_packed_multi_shot(): + """ + Tests the 'decode_shots_bit_packed' method with multiple shots. + """ + dem = stim.DetectorErrorModel(""" + detector(0, 0, 0) D0 + detector(0, 0, 1) D1 + detector(0, 0, 2) D2 + error(0.1) D0 D1 L0 + error(0.1) D1 D2 L1 + """) + + decoder = tesseract_module.TesseractSinterDecoder() + compiled_decoder = decoder.compile_decoder_for_dem(dem=dem) + + num_shots = 3 + detections_array = np.zeros((num_shots, (dem.num_detectors + 7) // 8), dtype=np.uint8) + + # Shot 0: D0 and D1 fire. Expect L0 to flip. + detections_array[0][0] |= (1 << 0) # D0 + detections_array[0][0] |= (1 << 1) # D1 + + # Shot 1: D1 and D2 fire. Expect L1 to flip. + detections_array[1][0] |= (1 << 1) # D1 + detections_array[1][0] |= (1 << 2) # D2 + + # Shot 2: D0 and D2 fire. Expect L0 and L1 to flip. + detections_array[2][0] |= (1 << 0) # D0 + detections_array[2][0] |= (1 << 2) # D2 + + predictions = compiled_decoder.decode_shots_bit_packed(bit_packed_detection_event_data=detections_array) + + expected_predictions = np.zeros((num_shots, (dem.num_observables + 7) // 8), dtype=np.uint8) + # Expected flip for shot 0 is L0 + expected_predictions[0][0] |= (1 << 0) + # Expected flip for shot 1 is L1 + expected_predictions[1][0] |= (1 << 1) + # Expected flip for shot 2 is L0 and L1 + expected_predictions[2][0] |= (1 << 0) + expected_predictions[2][0] |= (1 << 1) + + assert np.array_equal(predictions, expected_predictions) + + +def test_decode_via_files_sanity_check(): + """ + Tests the 'decode_via_files' method by simulating a small circuit and + checking for output files. + """ + + # Create a temporary directory for test files + temp_dir = pathlib.Path("./temp_test_files") + if temp_dir.exists(): + shutil.rmtree(temp_dir) + temp_dir.mkdir() + + dem_path = temp_dir / "test.dem" + dets_in_path = temp_dir / "test.b8" + obs_out_path = temp_dir / "test.out.b8" + + # Create a small circuit and DEM file + circuit = stim.Circuit.generated("repetition_code:memory", distance=3, rounds=2) + dem = circuit.detector_error_model() + with open(dem_path, 'w') as f: + f.write(str(dem)) + + # Generate dummy detection events and save to file + num_shots = 10 + sampler = circuit.compile_detector_sampler() + detection_events = sampler.sample(num_shots, bit_packed=True) + with open(dets_in_path, 'wb') as f: + f.write(detection_events.tobytes()) + + tesseract_module.TesseractSinterDecoder().decode_via_files( + num_shots=num_shots, + num_dets=dem.num_detectors, + num_obs=dem.num_observables, + dem_path=str(dem_path), + dets_b8_in_path=str(dets_in_path), + obs_predictions_b8_out_path=str(obs_out_path), + tmp_dir=str(temp_dir) + ) + + if temp_dir.exists(): + shutil.rmtree(temp_dir) + +def test_decode_via_files(): + """ + Tests the 'decode_via_files' method with a specific DEM and detection event. + """ + + # Create a temporary directory for test files + temp_dir = pathlib.Path("./temp_test_files") + if temp_dir.exists(): + shutil.rmtree(temp_dir) + temp_dir.mkdir() + + dem_path = temp_dir / "test.dem" + dets_in_path = temp_dir / "test.b8" + obs_out_path = temp_dir / "test.out.b8" + + # Create a specific DEM + dem_string = """ + detector(0, 0, 0) D0 + detector(0, 0, 1) D1 + detector(0, 0, 2) D2 + detector(0, 0, 3) D3 + error(0.1) D0 D1 L0 + error(0.1) D1 D2 L1 + error(0.1) D2 D3 L0 + """ + dem = stim.DetectorErrorModel(dem_string) + + # Write the DEM string to a file + with open(dem_path, 'w') as f: + f.write(dem_string) + + detections = [0, 1] + expected_predictions = np.zeros(dem.num_observables, dtype=np.uint8) + expected_predictions[0] = 1 # Flip on L0 + + # Pack the detection events into a bit-packed NumPy array + num_shots = 1 + num_detectors = dem.num_detectors + detection_events_np = np.zeros(num_shots * ((num_detectors + 7) // 8), dtype=np.uint8) + for d_idx in detections: + detection_events_np[d_idx // 8] ^= (1 << (d_idx % 8)) + + # Write the packed detection events to the input file + with open(dets_in_path, 'wb') as f: + f.write(detection_events_np.tobytes()) + + tesseract_module.TesseractSinterDecoder().decode_via_files( + num_shots=num_shots, + num_dets=num_detectors, + num_obs=dem.num_observables, + dem_path=str(dem_path), + dets_b8_in_path=str(dets_in_path), + obs_predictions_b8_out_path=str(obs_out_path), + tmp_dir=str(temp_dir) + ) + + # Read the output file and unpack the results + with open(obs_out_path, 'rb') as f: + predictions_bytes = f.read() + + # Convert bytes to a numpy array for easy comparison + predictions_np = np.frombuffer(predictions_bytes, dtype=np.uint8) + unpacked_predictions = np.zeros(dem.num_observables, dtype=np.uint8) + for i in range(dem.num_observables): + if (predictions_np[i // 8] >> (i % 8)) & 1: + unpacked_predictions[i] = 1 + + assert np.array_equal(unpacked_predictions, expected_predictions) + + # Clean up temporary files + if temp_dir.exists(): + shutil.rmtree(temp_dir) + +def test_decode_via_files_multi_shot(): + """ + Tests the 'decode_via_files' method with multiple shots and a specific DEM. + """ + # Create a temporary directory for test files + temp_dir = pathlib.Path("./temp_test_files") + if temp_dir.exists(): + shutil.rmtree(temp_dir) + temp_dir.mkdir() + + dem_path = temp_dir / "test.dem" + dets_in_path = temp_dir / "test.b8" + obs_out_path = temp_dir / "test.out.b8" + + # Create a specific DEM + dem_string = """ + detector(0, 0, 0) D0 + detector(0, 0, 1) D1 + detector(0, 0, 2) D2 + error(0.1) D0 D1 L0 + error(0.1) D1 D2 L1 + """ + dem = stim.DetectorErrorModel(dem_string) + + # Write the DEM string to a file + with open(dem_path, 'w') as f: + f.write(dem_string) + + num_shots = 3 + num_detectors = dem.num_detectors + detection_events_np = np.zeros(num_shots * ((num_detectors + 7) // 8), dtype=np.uint8) + + # Shot 0: D0 and D1 fire. Expected L0 flip. + detection_events_np[0] |= (1 << 0) + detection_events_np[0] |= (1 << 1) + + # Shot 1: D1 and D2 fire. Expected L1 flip. + detection_events_np[1] |= (1 << 1) + detection_events_np[1] |= (1 << 2) + + # Shot 2: D0 and D2 fire. Expected L0 and L1 flips. + detection_events_np[2] |= (1 << 0) + detection_events_np[2] |= (1 << 2) + + # Write the packed detection events to the input file + with open(dets_in_path, 'wb') as f: + f.write(detection_events_np.tobytes()) + + tesseract_module.TesseractSinterDecoder().decode_via_files( + num_shots=num_shots, + num_dets=num_detectors, + num_obs=dem.num_observables, + dem_path=str(dem_path), + dets_b8_in_path=str(dets_in_path), + obs_predictions_b8_out_path=str(obs_out_path), + tmp_dir=str(temp_dir) + ) + + # Read the output file and unpack the results + with open(obs_out_path, 'rb') as f: + predictions_bytes = f.read() + + predictions_np = np.frombuffer(predictions_bytes, dtype=np.uint8) + + expected_predictions_np = np.zeros(num_shots * ((dem.num_observables + 7) // 8), dtype=np.uint8) + expected_predictions_np[0] |= (1 << 0) + expected_predictions_np[1] |= (1 << 1) + expected_predictions_np[2] |= (1 << 0) + expected_predictions_np[2] |= (1 << 1) + + assert np.array_equal(predictions_np, expected_predictions_np) + + # Clean up temporary files + if temp_dir.exists(): + shutil.rmtree(temp_dir) + +if __name__ == "__main__": + raise SystemExit(pytest.main([__file__])) \ No newline at end of file diff --git a/src/tesseract_sinter_compat.pybind.cpp b/src/tesseract_sinter_compat.pybind.cpp new file mode 100644 index 00000000..d506ea67 --- /dev/null +++ b/src/tesseract_sinter_compat.pybind.cpp @@ -0,0 +1,295 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include +#include "stim.h" +#include "tesseract.h" + +namespace py = pybind11; + +// These are the classes that will be exposed to Python. +struct TesseractSinterCompiledDecoder; +struct TesseractSinterDecoder; + +//-------------------------------------------------------------------------------------------------- +// This struct implements the sinter.CompiledDecoder API. It holds the pre-compiled decoder +// instance and performs the actual decoding on bit-packed NumPy arrays. +//-------------------------------------------------------------------------------------------------- +struct TesseractSinterCompiledDecoder { + // A pointer to the pre-configured TesseractDecoder. + std::unique_ptr decoder; + uint64_t num_detectors; + uint64_t num_observables; + + // Decode a batch of syndrome shots in a bit-packed NumPy array. + py::array_t decode_shots_bit_packed( + const py::array_t& bit_packed_detection_event_data + ) { + // Validate input. + if (bit_packed_detection_event_data.ndim() != 2) { + throw std::invalid_argument("Input `bit_packed_detection_event_data` must be a 2D array."); + } + + // Calculate number of bytes per shot. + const uint64_t num_detector_bytes = (num_detectors + 7) / 8; + if (bit_packed_detection_event_data.shape(1) != (py::ssize_t)num_detector_bytes) { + throw std::invalid_argument("Input array's second dimension does not match num_detector_bytes."); + } + + const size_t num_shots = bit_packed_detection_event_data.shape(0); + const uint64_t num_observable_bytes = (num_observables + 7) / 8; + + // Result buffer to store the predicted observables for all shots. + auto result_array = py::array_t({(py::ssize_t)num_shots, (py::ssize_t)num_observable_bytes}); + auto result_buffer = result_array.mutable_data(); + + const uint8_t* detections_data = bit_packed_detection_event_data.data(); + const size_t detections_stride = bit_packed_detection_event_data.strides(0); + + // Loop through each shot and decode it with TesseractDecoder. + for (size_t shot = 0; shot < num_shots; ++shot) { + const uint8_t* single_shot_data = detections_data + shot * detections_stride; + + // Unpack the shot data into a vector of indices of fired detectors. + std::vector detections; + for (uint64_t i = 0; i < num_detectors; ++i) { + if ((single_shot_data[i / 8] >> (i % 8)) & 1) { + detections.push_back(i); + } + } + + // Decode with TesseractDecoder. + 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); + for (int obs_index : predictions) { + if (obs_index >= 0 && obs_index < num_observables) { + single_result_buffer[obs_index / 8] ^= (1 << (obs_index % 8)); + } + } + } + + // Return the result. + return result_array; + } +}; + +//-------------------------------------------------------------------------------------------------- +// This struct implements the sinter.Decoder API. It is responsible for creating and compiling +// a decoder for a specific Detector Error Model (DEM). +//-------------------------------------------------------------------------------------------------- +struct TesseractSinterDecoder { + TesseractSinterDecoder() {} + + bool operator==(const TesseractSinterDecoder& other) const { + return true; + } + + bool operator!=(const TesseractSinterDecoder& other) const { + return !(*this == other); + } + + // Take a string representation of the DEM, parse the DEM and return a compiled decoder instance. + TesseractSinterCompiledDecoder compile_decoder_for_dem(const py::object& dem) { + const stim::DetectorErrorModel stim_dem(py::cast(py::str(dem)).c_str()); + + TesseractConfig config; + config.dem = stim_dem; + + auto decoder = std::make_unique(config); + + return TesseractSinterCompiledDecoder { + .decoder = std::move(decoder), + .num_detectors = stim_dem.count_detectors(), + .num_observables = stim_dem.count_observables(), + }; + + } + + // Decode shots while operating on files that store the DEM information. + void decode_via_files( + uint64_t num_shots, + uint64_t num_dets, + uint64_t num_obs, + const py::object& dem_path, + const py::object& dets_b8_in_path, + const py::object& obs_predictions_b8_out_path, + const py::object& tmp_dir + ) { + std::string dem_path_str = py::cast(py::str(dem_path)); + std::string dets_in_str = py::cast(py::str(dets_b8_in_path)); + std::string obs_out_str = py::cast(py::str(obs_predictions_b8_out_path)); + + // Read the DEM from the file. + std::ifstream dem_file(dem_path_str); + std::stringstream dem_content_stream; + if (!dem_file) { + throw std::runtime_error("Failed to open DEM file: " + dem_path_str); + } + dem_content_stream << dem_file.rdbuf(); + std::string dem_content_str = dem_content_stream.str(); + dem_file.close(); + + // Construct TesseractDecoder. + const stim::DetectorErrorModel stim_dem(dem_content_str.c_str()); + TesseractConfig config; + config.dem = stim_dem; + TesseractDecoder decoder(config); + + // Calculate expected number of bytes per shot for detectors and observables. + const uint64_t num_detector_bytes = (num_dets + 7) / 8; + const uint64_t num_observable_bytes = (num_obs + 7) / 8; + + std::ifstream input_file(dets_in_str, std::ios::binary); + if (!input_file) { + throw std::runtime_error("Failed to open input file: " + dets_in_str); + } + std::ofstream output_file(obs_out_str, std::ios::binary); + if (!output_file) { + throw std::runtime_error("Failed to open output file: " + obs_out_str); + } + + std::vector single_shot_data(num_detector_bytes); + std::vector single_result_data(num_observable_bytes); + + for (uint64_t shot = 0; shot < num_shots; ++shot) { + // Read shot's data. + input_file.read(reinterpret_cast(single_shot_data.data()), num_detector_bytes); + if (input_file.gcount() != (std::streamsize)num_detector_bytes) { + throw std::runtime_error("Failed to read a full shot from the input file."); + } + + // Extract shot's data and parse into detector indices. + std::vector detections; + for (uint64_t i = 0; i < num_dets; ++i) { + if ((single_shot_data[i / 8] >> (i % 8)) & 1) { + detections.push_back(i); + } + } + + std::vector predictions = decoder.decode(detections); + + // 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) { + if (obs_index >= 0 && obs_index < num_obs) { + single_result_data[obs_index / 8] ^= (1 << (obs_index % 8)); + } + + } + + // Write result to the output file. + output_file.write(reinterpret_cast(single_result_data.data()), num_observable_bytes); + } + + input_file.close(); + output_file.close(); + } +}; + +//-------------------------------------------------------------------------------------------------- +// Expose C++ classes to the Python interpreter. +//-------------------------------------------------------------------------------------------------- +void pybind_sinter_compat(py::module& m) { + m.doc() = R"pbdoc( + This module provides Python bindings for the Tesseract quantum error + correction decoder, designed for compatibility with the Sinter library. + )pbdoc"; + + // Bind the TesseractSinterCompiledDecoder. + py::class_(m, "TesseractSinterCompiledDecoder", R"pbdoc( + A Tesseract decoder preconfigured for a specific Detector Error Model. + )pbdoc") + .def("decode_shots_bit_packed", &TesseractSinterCompiledDecoder::decode_shots_bit_packed, + py::kw_only(), py::arg("bit_packed_detection_event_data"), + R"pbdoc( + Predicts observable flips from bit-packed detection events. + + This function decodes a batch of `num_shots` syndrome measurements, + where each shot's detection events are provided in a bit-packed format. + + :param bit_packed_detection_event_data: A 2D numpy array of shape + `(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. + )pbdoc") + .def_readwrite("num_detectors", &TesseractSinterCompiledDecoder::num_detectors, R"pbdoc(The number of detectors in the decoder's underlying DEM.)pbdoc") + .def_readwrite("num_observables", &TesseractSinterCompiledDecoder::num_observables, R"pbdoc(The number of logical observables in the decoder's underlying DEM.)pbdoc"); + + // Bind the TesseractSinterDecoder. + py::class_(m, "TesseractSinterDecoder", R"pbdoc( + A factory for creating Tesseract decoders compatible with `sinter`. + )pbdoc") + .def(py::init<>(), R"pbdoc(Initializes a new TesseractSinterDecoder instance.)pbdoc") + .def("compile_decoder_for_dem", &TesseractSinterDecoder::compile_decoder_for_dem, + py::kw_only(), py::arg("dem"), + R"pbdoc( + Creates a Tesseract decoder preconfigured for the given detector error model. + + :param dem: The `stim.DetectorErrorModel` to configure the decoder for. + :return: A `TesseractSinterCompiledDecoder` instance that can decode + bit-packed shots for the given DEM. + )pbdoc") + .def("decode_via_files", &TesseractSinterDecoder::decode_via_files, + py::kw_only(), + py::arg("num_shots"), + py::arg("num_dets"), + py::arg("num_obs"), + py::arg("dem_path"), + py::arg("dets_b8_in_path"), + py::arg("obs_predictions_b8_out_path"), + py::arg("tmp_dir"), R"pbdoc( + Decodes data from files and writes the result to a file. + + :param num_shots: The number of shots to decode. + :param num_dets: The number of detectors in the error model. + :param num_obs: The number of logical observables in the error model. + :param dem_path: The path to a file containing the `stim.DetectorErrorModel` string. + :param dets_b8_in_path: The path to a file containing bit-packed detection events. + :param obs_predictions_b8_out_path: The path to the output file where + bit-packed observable predictions will be written. + :param tmp_dir: A temporary directory path. (Currently unused, but required by API) + )pbdoc") + .def(py::self == py::self, R"pbdoc(Checks if two TesseractSinterDecoder instances are equal.)pbdoc") + .def(py::self != py::self, R"pbdoc(Checks if two TesseractSinterDecoder instances are not equal.)pbdoc"); + + // Define the sinter_decoders function, which is the entry point for sinter. + m.def("sinter_decoders", []() { + py::dict result; + result["tesseract"] = TesseractSinterDecoder(); + return result; + }, + R"pbdoc( + Returns a dictionary of available Sinter-compatible decoders. + + This function is the main entry point for the Sinter library's custom decoder + integration. It returns a dictionary where keys are decoder names (e.g., "tesseract") + and values are corresponding `sinter.Decoder` objects. + )pbdoc"); +} + +// Node: I need this macro, which will create the entry point function that +// Python interpreter call when I import this module. +PYBIND11_MODULE(tesseract_sinter_compat, m) { + pybind_sinter_compat(m); +} From 75068c9b38fb2ba410bb4e80b62d580ffeec1695 Mon Sep 17 00:00:00 2001 From: Dragana Grbic Date: Sat, 9 Aug 2025 17:31:26 -0700 Subject: [PATCH 02/14] Minor --- src/py/tesseract_sinter_compat_test.py | 14 + src/tesseract_sinter_compat.pybind.cpp | 371 ++++++++++++------------- 2 files changed, 198 insertions(+), 187 deletions(-) diff --git a/src/py/tesseract_sinter_compat_test.py b/src/py/tesseract_sinter_compat_test.py index a126d890..7cafe1e8 100644 --- a/src/py/tesseract_sinter_compat_test.py +++ b/src/py/tesseract_sinter_compat_test.py @@ -1,3 +1,17 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import pathlib import pytest import numpy as np diff --git a/src/tesseract_sinter_compat.pybind.cpp b/src/tesseract_sinter_compat.pybind.cpp index d506ea67..86585445 100644 --- a/src/tesseract_sinter_compat.pybind.cpp +++ b/src/tesseract_sinter_compat.pybind.cpp @@ -12,12 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include -#include +#include #include #include -#include +#include + #include +#include + #include "stim.h" #include "tesseract.h" @@ -32,64 +34,65 @@ struct TesseractSinterDecoder; // instance and performs the actual decoding on bit-packed NumPy arrays. //-------------------------------------------------------------------------------------------------- struct TesseractSinterCompiledDecoder { - // A pointer to the pre-configured TesseractDecoder. - std::unique_ptr decoder; - uint64_t num_detectors; - uint64_t num_observables; - - // Decode a batch of syndrome shots in a bit-packed NumPy array. - py::array_t decode_shots_bit_packed( - const py::array_t& bit_packed_detection_event_data - ) { - // Validate input. - if (bit_packed_detection_event_data.ndim() != 2) { - throw std::invalid_argument("Input `bit_packed_detection_event_data` must be a 2D array."); - } + // A pointer to the pre-configured TesseractDecoder. + std::unique_ptr decoder; + uint64_t num_detectors; + uint64_t num_observables; + + // Decode a batch of syndrome shots in a bit-packed NumPy array. + py::array_t decode_shots_bit_packed( + const py::array_t& bit_packed_detection_event_data) { + // Validate input. + if (bit_packed_detection_event_data.ndim() != 2) { + throw std::invalid_argument("Input `bit_packed_detection_event_data` must be a 2D array."); + } - // Calculate number of bytes per shot. - const uint64_t num_detector_bytes = (num_detectors + 7) / 8; - if (bit_packed_detection_event_data.shape(1) != (py::ssize_t)num_detector_bytes) { - throw std::invalid_argument("Input array's second dimension does not match num_detector_bytes."); - } + // Calculate number of bytes per shot. + const uint64_t num_detector_bytes = (num_detectors + 7) / 8; + if (bit_packed_detection_event_data.shape(1) != (py::ssize_t)num_detector_bytes) { + throw std::invalid_argument( + "Input array's second dimension does not match num_detector_bytes."); + } + + const size_t num_shots = bit_packed_detection_event_data.shape(0); + const uint64_t num_observable_bytes = (num_observables + 7) / 8; + + // Result buffer to store the predicted observables for all shots. + auto result_array = + py::array_t({(py::ssize_t)num_shots, (py::ssize_t)num_observable_bytes}); + auto result_buffer = result_array.mutable_data(); - const size_t num_shots = bit_packed_detection_event_data.shape(0); - const uint64_t num_observable_bytes = (num_observables + 7) / 8; - - // Result buffer to store the predicted observables for all shots. - auto result_array = py::array_t({(py::ssize_t)num_shots, (py::ssize_t)num_observable_bytes}); - auto result_buffer = result_array.mutable_data(); - - const uint8_t* detections_data = bit_packed_detection_event_data.data(); - const size_t detections_stride = bit_packed_detection_event_data.strides(0); - - // Loop through each shot and decode it with TesseractDecoder. - for (size_t shot = 0; shot < num_shots; ++shot) { - const uint8_t* single_shot_data = detections_data + shot * detections_stride; - - // Unpack the shot data into a vector of indices of fired detectors. - std::vector detections; - for (uint64_t i = 0; i < num_detectors; ++i) { - if ((single_shot_data[i / 8] >> (i % 8)) & 1) { - detections.push_back(i); - } - } - - // Decode with TesseractDecoder. - 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); - for (int obs_index : predictions) { - if (obs_index >= 0 && obs_index < num_observables) { - single_result_buffer[obs_index / 8] ^= (1 << (obs_index % 8)); - } - } + const uint8_t* detections_data = bit_packed_detection_event_data.data(); + const size_t detections_stride = bit_packed_detection_event_data.strides(0); + + // Loop through each shot and decode it with TesseractDecoder. + for (size_t shot = 0; shot < num_shots; ++shot) { + const uint8_t* single_shot_data = detections_data + shot * detections_stride; + + // Unpack the shot data into a vector of indices of fired detectors. + std::vector detections; + for (uint64_t i = 0; i < num_detectors; ++i) { + if ((single_shot_data[i / 8] >> (i % 8)) & 1) { + detections.push_back(i); } + } - // Return the result. - return result_array; + // Decode with TesseractDecoder. + 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); + for (int obs_index : predictions) { + if (obs_index >= 0 && obs_index < num_observables) { + single_result_buffer[obs_index / 8] ^= (1 << (obs_index % 8)); + } + } } + + // Return the result. + return result_array; + } }; //-------------------------------------------------------------------------------------------------- @@ -97,130 +100,122 @@ struct TesseractSinterCompiledDecoder { // a decoder for a specific Detector Error Model (DEM). //-------------------------------------------------------------------------------------------------- struct TesseractSinterDecoder { - TesseractSinterDecoder() {} - - bool operator==(const TesseractSinterDecoder& other) const { - return true; + TesseractSinterDecoder() {} + + bool operator==(const TesseractSinterDecoder& other) const { + return true; + } + + bool operator!=(const TesseractSinterDecoder& other) const { + return !(*this == other); + } + + // Take a string representation of the DEM, parse the DEM and return a compiled decoder instance. + TesseractSinterCompiledDecoder compile_decoder_for_dem(const py::object& dem) { + const stim::DetectorErrorModel stim_dem(py::cast(py::str(dem)).c_str()); + + TesseractConfig config; + config.dem = stim_dem; + + auto decoder = std::make_unique(config); + + return TesseractSinterCompiledDecoder{ + .decoder = std::move(decoder), + .num_detectors = stim_dem.count_detectors(), + .num_observables = stim_dem.count_observables(), + }; + } + + // Decode shots while operating on files that store the DEM information. + void decode_via_files(uint64_t num_shots, uint64_t num_dets, uint64_t num_obs, + const py::object& dem_path, const py::object& dets_b8_in_path, + const py::object& obs_predictions_b8_out_path, const py::object& tmp_dir) { + std::string dem_path_str = py::cast(py::str(dem_path)); + std::string dets_in_str = py::cast(py::str(dets_b8_in_path)); + std::string obs_out_str = py::cast(py::str(obs_predictions_b8_out_path)); + + // Read the DEM from the file. + std::ifstream dem_file(dem_path_str); + std::stringstream dem_content_stream; + if (!dem_file) { + throw std::runtime_error("Failed to open DEM file: " + dem_path_str); } - - bool operator!=(const TesseractSinterDecoder& other) const { - return !(*this == other); + dem_content_stream << dem_file.rdbuf(); + std::string dem_content_str = dem_content_stream.str(); + dem_file.close(); + + // Construct TesseractDecoder. + const stim::DetectorErrorModel stim_dem(dem_content_str.c_str()); + TesseractConfig config; + config.dem = stim_dem; + TesseractDecoder decoder(config); + + // Calculate expected number of bytes per shot for detectors and observables. + const uint64_t num_detector_bytes = (num_dets + 7) / 8; + const uint64_t num_observable_bytes = (num_obs + 7) / 8; + + std::ifstream input_file(dets_in_str, std::ios::binary); + if (!input_file) { + throw std::runtime_error("Failed to open input file: " + dets_in_str); } - - // Take a string representation of the DEM, parse the DEM and return a compiled decoder instance. - TesseractSinterCompiledDecoder compile_decoder_for_dem(const py::object& dem) { - const stim::DetectorErrorModel stim_dem(py::cast(py::str(dem)).c_str()); - - TesseractConfig config; - config.dem = stim_dem; - - auto decoder = std::make_unique(config); - - return TesseractSinterCompiledDecoder { - .decoder = std::move(decoder), - .num_detectors = stim_dem.count_detectors(), - .num_observables = stim_dem.count_observables(), - }; - + std::ofstream output_file(obs_out_str, std::ios::binary); + if (!output_file) { + throw std::runtime_error("Failed to open output file: " + obs_out_str); } - - // Decode shots while operating on files that store the DEM information. - void decode_via_files( - uint64_t num_shots, - uint64_t num_dets, - uint64_t num_obs, - const py::object& dem_path, - const py::object& dets_b8_in_path, - const py::object& obs_predictions_b8_out_path, - const py::object& tmp_dir - ) { - std::string dem_path_str = py::cast(py::str(dem_path)); - std::string dets_in_str = py::cast(py::str(dets_b8_in_path)); - std::string obs_out_str = py::cast(py::str(obs_predictions_b8_out_path)); - - // Read the DEM from the file. - std::ifstream dem_file(dem_path_str); - std::stringstream dem_content_stream; - if (!dem_file) { - throw std::runtime_error("Failed to open DEM file: " + dem_path_str); - } - dem_content_stream << dem_file.rdbuf(); - std::string dem_content_str = dem_content_stream.str(); - dem_file.close(); - - // Construct TesseractDecoder. - const stim::DetectorErrorModel stim_dem(dem_content_str.c_str()); - TesseractConfig config; - config.dem = stim_dem; - TesseractDecoder decoder(config); - - // Calculate expected number of bytes per shot for detectors and observables. - const uint64_t num_detector_bytes = (num_dets + 7) / 8; - const uint64_t num_observable_bytes = (num_obs + 7) / 8; - - std::ifstream input_file(dets_in_str, std::ios::binary); - if (!input_file) { - throw std::runtime_error("Failed to open input file: " + dets_in_str); - } - std::ofstream output_file(obs_out_str, std::ios::binary); - if (!output_file) { - throw std::runtime_error("Failed to open output file: " + obs_out_str); + + std::vector single_shot_data(num_detector_bytes); + std::vector single_result_data(num_observable_bytes); + + for (uint64_t shot = 0; shot < num_shots; ++shot) { + // Read shot's data. + input_file.read(reinterpret_cast(single_shot_data.data()), num_detector_bytes); + if (input_file.gcount() != (std::streamsize)num_detector_bytes) { + throw std::runtime_error("Failed to read a full shot from the input file."); + } + + // Extract shot's data and parse into detector indices. + std::vector detections; + for (uint64_t i = 0; i < num_dets; ++i) { + if ((single_shot_data[i / 8] >> (i % 8)) & 1) { + detections.push_back(i); } + } + + std::vector predictions = decoder.decode(detections); - std::vector single_shot_data(num_detector_bytes); - std::vector single_result_data(num_observable_bytes); - - for (uint64_t shot = 0; shot < num_shots; ++shot) { - // Read shot's data. - input_file.read(reinterpret_cast(single_shot_data.data()), num_detector_bytes); - if (input_file.gcount() != (std::streamsize)num_detector_bytes) { - throw std::runtime_error("Failed to read a full shot from the input file."); - } - - // Extract shot's data and parse into detector indices. - std::vector detections; - for (uint64_t i = 0; i < num_dets; ++i) { - if ((single_shot_data[i / 8] >> (i % 8)) & 1) { - detections.push_back(i); - } - } - - std::vector predictions = decoder.decode(detections); - - // 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) { - if (obs_index >= 0 && obs_index < num_obs) { - single_result_data[obs_index / 8] ^= (1 << (obs_index % 8)); - } - - } - - // Write result to the output file. - output_file.write(reinterpret_cast(single_result_data.data()), num_observable_bytes); + // 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) { + if (obs_index >= 0 && obs_index < num_obs) { + single_result_data[obs_index / 8] ^= (1 << (obs_index % 8)); } + } - input_file.close(); - output_file.close(); + // Write result to the output file. + output_file.write(reinterpret_cast(single_result_data.data()), num_observable_bytes); } + + input_file.close(); + output_file.close(); + } }; //-------------------------------------------------------------------------------------------------- // Expose C++ classes to the Python interpreter. //-------------------------------------------------------------------------------------------------- void pybind_sinter_compat(py::module& m) { - m.doc() = R"pbdoc( + m.doc() = R"pbdoc( This module provides Python bindings for the Tesseract quantum error correction decoder, designed for compatibility with the Sinter library. )pbdoc"; - // Bind the TesseractSinterCompiledDecoder. - py::class_(m, "TesseractSinterCompiledDecoder", R"pbdoc( + // Bind the TesseractSinterCompiledDecoder. + py::class_(m, "TesseractSinterCompiledDecoder", R"pbdoc( A Tesseract decoder preconfigured for a specific Detector Error Model. )pbdoc") - .def("decode_shots_bit_packed", &TesseractSinterCompiledDecoder::decode_shots_bit_packed, - py::kw_only(), py::arg("bit_packed_detection_event_data"), - R"pbdoc( + .def("decode_shots_bit_packed", &TesseractSinterCompiledDecoder::decode_shots_bit_packed, + py::kw_only(), py::arg("bit_packed_detection_event_data"), + R"pbdoc( Predicts observable flips from bit-packed detection events. This function decodes a batch of `num_shots` syndrome measurements, @@ -233,32 +228,30 @@ void pybind_sinter_compat(py::module& m) { :return: A 2D numpy array of shape `(num_shots, ceil(num_observables / 8))` containing the predicted observable flips in a bit-packed format. )pbdoc") - .def_readwrite("num_detectors", &TesseractSinterCompiledDecoder::num_detectors, R"pbdoc(The number of detectors in the decoder's underlying DEM.)pbdoc") - .def_readwrite("num_observables", &TesseractSinterCompiledDecoder::num_observables, R"pbdoc(The number of logical observables in the decoder's underlying DEM.)pbdoc"); - - // Bind the TesseractSinterDecoder. - py::class_(m, "TesseractSinterDecoder", R"pbdoc( + .def_readwrite("num_detectors", &TesseractSinterCompiledDecoder::num_detectors, + R"pbdoc(The number of detectors in the decoder's underlying DEM.)pbdoc") + .def_readwrite( + "num_observables", &TesseractSinterCompiledDecoder::num_observables, + R"pbdoc(The number of logical observables in the decoder's underlying DEM.)pbdoc"); + + // Bind the TesseractSinterDecoder. + py::class_(m, "TesseractSinterDecoder", R"pbdoc( A factory for creating Tesseract decoders compatible with `sinter`. )pbdoc") - .def(py::init<>(), R"pbdoc(Initializes a new TesseractSinterDecoder instance.)pbdoc") - .def("compile_decoder_for_dem", &TesseractSinterDecoder::compile_decoder_for_dem, - py::kw_only(), py::arg("dem"), - R"pbdoc( + .def(py::init<>(), R"pbdoc(Initializes a new TesseractSinterDecoder instance.)pbdoc") + .def("compile_decoder_for_dem", &TesseractSinterDecoder::compile_decoder_for_dem, + py::kw_only(), py::arg("dem"), + R"pbdoc( Creates a Tesseract decoder preconfigured for the given detector error model. :param dem: The `stim.DetectorErrorModel` to configure the decoder for. :return: A `TesseractSinterCompiledDecoder` instance that can decode bit-packed shots for the given DEM. )pbdoc") - .def("decode_via_files", &TesseractSinterDecoder::decode_via_files, - py::kw_only(), - py::arg("num_shots"), - py::arg("num_dets"), - py::arg("num_obs"), - py::arg("dem_path"), - py::arg("dets_b8_in_path"), - py::arg("obs_predictions_b8_out_path"), - py::arg("tmp_dir"), R"pbdoc( + .def("decode_via_files", &TesseractSinterDecoder::decode_via_files, py::kw_only(), + py::arg("num_shots"), py::arg("num_dets"), py::arg("num_obs"), py::arg("dem_path"), + py::arg("dets_b8_in_path"), py::arg("obs_predictions_b8_out_path"), py::arg("tmp_dir"), + R"pbdoc( Decodes data from files and writes the result to a file. :param num_shots: The number of shots to decode. @@ -270,16 +263,20 @@ void pybind_sinter_compat(py::module& m) { bit-packed observable predictions will be written. :param tmp_dir: A temporary directory path. (Currently unused, but required by API) )pbdoc") - .def(py::self == py::self, R"pbdoc(Checks if two TesseractSinterDecoder instances are equal.)pbdoc") - .def(py::self != py::self, R"pbdoc(Checks if two TesseractSinterDecoder instances are not equal.)pbdoc"); - - // Define the sinter_decoders function, which is the entry point for sinter. - m.def("sinter_decoders", []() { + .def(py::self == py::self, + R"pbdoc(Checks if two TesseractSinterDecoder instances are equal.)pbdoc") + .def(py::self != py::self, + R"pbdoc(Checks if two TesseractSinterDecoder instances are not equal.)pbdoc"); + + // Define the sinter_decoders function, which is the entry point for sinter. + m.def( + "sinter_decoders", + []() { py::dict result; result["tesseract"] = TesseractSinterDecoder(); return result; - }, - R"pbdoc( + }, + R"pbdoc( Returns a dictionary of available Sinter-compatible decoders. This function is the main entry point for the Sinter library's custom decoder @@ -288,8 +285,8 @@ void pybind_sinter_compat(py::module& m) { )pbdoc"); } -// Node: I need this macro, which will create the entry point function that +// Node: I need this macro, which will create the entry point function that // Python interpreter call when I import this module. PYBIND11_MODULE(tesseract_sinter_compat, m) { - pybind_sinter_compat(m); + pybind_sinter_compat(m); } From 1f34dd933b75b3e68af0c436a533bf33b45fb43d Mon Sep 17 00:00:00 2001 From: Dragana Grbic Date: Sun, 10 Aug 2025 17:46:30 -0700 Subject: [PATCH 03/14] Minor change --- src/BUILD | 2 +- ...nter_compat.pybind.cpp => tesseract_sinter_compat.pybind.cc} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{tesseract_sinter_compat.pybind.cpp => tesseract_sinter_compat.pybind.cc} (100%) diff --git a/src/BUILD b/src/BUILD index 0556a2c9..6a7b5a7a 100644 --- a/src/BUILD +++ b/src/BUILD @@ -92,7 +92,7 @@ pybind_extension( pybind_extension( name = "tesseract_sinter_compat", - srcs = ["tesseract_sinter_compat.pybind.cpp"], + srcs = ["tesseract_sinter_compat.pybind.cc"], deps = [ ":libtesseract", ], diff --git a/src/tesseract_sinter_compat.pybind.cpp b/src/tesseract_sinter_compat.pybind.cc similarity index 100% rename from src/tesseract_sinter_compat.pybind.cpp rename to src/tesseract_sinter_compat.pybind.cc From d4ab871139d0bd55bc4066752612acae5340db9a Mon Sep 17 00:00:00 2001 From: Dragana Grbic Date: Mon, 11 Aug 2025 16:03:18 -0700 Subject: [PATCH 04/14] Test integration with Sinter, include Sinter API inside tesseract library, bind tesseract decoding config inside Sinter API --- src/BUILD | 10 +- src/py/BUILD | 1 + src/py/requirements.in | 1 + src/py/requirements_lock.txt | 478 +++++++++++++++++- src/py/tesseract_sinter_compat_test.py | 188 ++++++- src/tesseract.pybind.cc | 2 + src/tesseract.pybind.h | 4 + ...nd.cc => tesseract_sinter_compat.pybind.h} | 71 +-- 8 files changed, 699 insertions(+), 56 deletions(-) rename src/{tesseract_sinter_compat.pybind.cc => tesseract_sinter_compat.pybind.h} (88%) diff --git a/src/BUILD b/src/BUILD index 99321205..e76e2970 100644 --- a/src/BUILD +++ b/src/BUILD @@ -72,6 +72,7 @@ pybind_library( "simplex.pybind.h", "visualization.pybind.h", "tesseract.pybind.h", + "tesseract_sinter_compat.pybind.h", ], deps = [ ":libcommon", @@ -91,20 +92,11 @@ pybind_extension( ], ) -pybind_extension( - name = "tesseract_sinter_compat", - srcs = ["tesseract_sinter_compat.pybind.cc"], - deps = [ - ":libtesseract", - ], -) - py_library( name="lib_tesseract_decoder", imports=["src"], deps=[ ":tesseract_decoder", - ":tesseract_sinter_compat", ], ) diff --git a/src/py/BUILD b/src/py/BUILD index a4946c6f..31cbcf0d 100644 --- a/src/py/BUILD +++ b/src/py/BUILD @@ -65,6 +65,7 @@ py_test( deps = [ "@pypi//pytest", "@pypi//stim", + "@pypi//sinter", "//src:lib_tesseract_decoder", ], ) diff --git a/src/py/requirements.in b/src/py/requirements.in index 031a56aa..5f620020 100644 --- a/src/py/requirements.in +++ b/src/py/requirements.in @@ -1,2 +1,3 @@ stim pytest +sinter \ No newline at end of file diff --git a/src/py/requirements_lock.txt b/src/py/requirements_lock.txt index 7f96c6ed..20ca0e0f 100644 --- a/src/py/requirements_lock.txt +++ b/src/py/requirements_lock.txt @@ -4,10 +4,292 @@ # # bazel run //src/py:requirements.update # +contourpy==1.3.3 \ + --hash=sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69 \ + --hash=sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc \ + --hash=sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880 \ + --hash=sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a \ + --hash=sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8 \ + --hash=sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc \ + --hash=sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470 \ + --hash=sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5 \ + --hash=sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263 \ + --hash=sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b \ + --hash=sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5 \ + --hash=sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381 \ + --hash=sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3 \ + --hash=sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4 \ + --hash=sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e \ + --hash=sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f \ + --hash=sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772 \ + --hash=sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286 \ + --hash=sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42 \ + --hash=sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301 \ + --hash=sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77 \ + --hash=sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7 \ + --hash=sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411 \ + --hash=sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1 \ + --hash=sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9 \ + --hash=sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a \ + --hash=sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b \ + --hash=sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db \ + --hash=sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6 \ + --hash=sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620 \ + --hash=sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989 \ + --hash=sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea \ + --hash=sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67 \ + --hash=sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5 \ + --hash=sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d \ + --hash=sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36 \ + --hash=sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99 \ + --hash=sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1 \ + --hash=sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e \ + --hash=sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b \ + --hash=sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8 \ + --hash=sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d \ + --hash=sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7 \ + --hash=sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7 \ + --hash=sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339 \ + --hash=sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1 \ + --hash=sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659 \ + --hash=sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4 \ + --hash=sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f \ + --hash=sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20 \ + --hash=sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36 \ + --hash=sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb \ + --hash=sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d \ + --hash=sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8 \ + --hash=sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0 \ + --hash=sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b \ + --hash=sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7 \ + --hash=sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe \ + --hash=sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77 \ + --hash=sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497 \ + --hash=sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd \ + --hash=sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1 \ + --hash=sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216 \ + --hash=sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13 \ + --hash=sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae \ + --hash=sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae \ + --hash=sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77 \ + --hash=sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3 \ + --hash=sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f \ + --hash=sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff \ + --hash=sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9 \ + --hash=sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a + # via matplotlib +cycler==0.12.1 \ + --hash=sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 \ + --hash=sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c + # via matplotlib +fonttools==4.59.0 \ + --hash=sha256:052444a5d0151878e87e3e512a1aa1a0ab35ee4c28afde0a778e23b0ace4a7de \ + --hash=sha256:169b99a2553a227f7b5fea8d9ecd673aa258617f466b2abc6091fe4512a0dcd0 \ + --hash=sha256:209b75943d158f610b78320eacb5539aa9e920bee2c775445b2846c65d20e19d \ + --hash=sha256:21e606b2d38fed938dde871c5736822dd6bda7a4631b92e509a1f5cd1b90c5df \ + --hash=sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d \ + --hash=sha256:26731739daa23b872643f0e4072d5939960237d540c35c14e6a06d47d71ca8fe \ + --hash=sha256:2e7cf8044ce2598bb87e44ba1d2c6e45d7a8decf56055b92906dc53f67c76d64 \ + --hash=sha256:31003b6a10f70742a63126b80863ab48175fb8272a18ca0846c0482968f0588e \ + --hash=sha256:332bfe685d1ac58ca8d62b8d6c71c2e52a6c64bc218dc8f7825c9ea51385aa01 \ + --hash=sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705 \ + --hash=sha256:37e01c6ec0c98599778c2e688350d624fa4770fbd6144551bd5e032f1199171c \ + --hash=sha256:401b1941ce37e78b8fd119b419b617277c65ae9417742a63282257434fd68ea2 \ + --hash=sha256:4536f2695fe5c1ffb528d84a35a7d3967e5558d2af58b4775e7ab1449d65767b \ + --hash=sha256:4c908a7036f0f3677f8afa577bcd973e3e20ddd2f7c42a33208d18bee95cdb6f \ + --hash=sha256:51ab1ff33c19e336c02dee1e9fd1abd974a4ca3d8f7eef2a104d0816a241ce97 \ + --hash=sha256:524133c1be38445c5c0575eacea42dbd44374b310b1ffc4b60ff01d881fabb96 \ + --hash=sha256:57bb7e26928573ee7c6504f54c05860d867fd35e675769f3ce01b52af38d48e2 \ + --hash=sha256:60f6665579e909b618282f3c14fa0b80570fbf1ee0e67678b9a9d43aa5d67a37 \ + --hash=sha256:62224a9bb85b4b66d1b46d45cbe43d71cbf8f527d332b177e3b96191ffbc1e64 \ + --hash=sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757 \ + --hash=sha256:6801aeddb6acb2c42eafa45bc1cb98ba236871ae6f33f31e984670b749a8e58e \ + --hash=sha256:70d6b3ceaa9cc5a6ac52884f3b3d9544e8e231e95b23f138bdb78e6d4dc0eae3 \ + --hash=sha256:78813b49d749e1bb4db1c57f2d4d7e6db22c253cb0a86ad819f5dc197710d4b2 \ + --hash=sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c \ + --hash=sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0 \ + --hash=sha256:885bde7d26e5b40e15c47bd5def48b38cbd50830a65f98122a8fb90962af7cd1 \ + --hash=sha256:8b4309a2775e4feee7356e63b163969a215d663399cce1b3d3b65e7ec2d9680e \ + --hash=sha256:8d77f92438daeaddc05682f0f3dac90c5b9829bcac75b57e8ce09cb67786073c \ + --hash=sha256:902425f5afe28572d65d2bf9c33edd5265c612ff82c69e6f83ea13eafc0dcbea \ + --hash=sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5 \ + --hash=sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6 \ + --hash=sha256:a9bf8adc9e1f3012edc8f09b08336272aec0c55bc677422273e21280db748f7c \ + --hash=sha256:b818db35879d2edf7f46c7e729c700a0bce03b61b9412f5a7118406687cb151d \ + --hash=sha256:b8974b2a266b54c96709bd5e239979cddfd2dbceed331aa567ea1d7c4a2202db \ + --hash=sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14 \ + --hash=sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38 \ + --hash=sha256:d40dcf533ca481355aa7b682e9e079f766f35715defa4929aeb5597f9604272e \ + --hash=sha256:e93df708c69a193fc7987192f94df250f83f3851fda49413f02ba5dded639482 \ + --hash=sha256:efd7e6660674e234e29937bc1481dceb7e0336bfae75b856b4fb272b5093c5d4 \ + --hash=sha256:f9b3a78f69dcbd803cf2fb3f972779875b244c1115481dfbdd567b2c22b31f6b \ + --hash=sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464 \ + --hash=sha256:fbce6dae41b692a5973d0f2158f782b9ad05babc2c2019a970a1094a23909b1b + # via matplotlib iniconfig==2.1.0 \ --hash=sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7 \ --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 # via pytest +kiwisolver==1.4.9 \ + --hash=sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c \ + --hash=sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7 \ + --hash=sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21 \ + --hash=sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e \ + --hash=sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff \ + --hash=sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7 \ + --hash=sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c \ + --hash=sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26 \ + --hash=sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa \ + --hash=sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f \ + --hash=sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1 \ + --hash=sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891 \ + --hash=sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77 \ + --hash=sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543 \ + --hash=sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d \ + --hash=sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce \ + --hash=sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3 \ + --hash=sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60 \ + --hash=sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a \ + --hash=sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089 \ + --hash=sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab \ + --hash=sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78 \ + --hash=sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771 \ + --hash=sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f \ + --hash=sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b \ + --hash=sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14 \ + --hash=sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32 \ + --hash=sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527 \ + --hash=sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185 \ + --hash=sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634 \ + --hash=sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed \ + --hash=sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1 \ + --hash=sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c \ + --hash=sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11 \ + --hash=sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752 \ + --hash=sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5 \ + --hash=sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4 \ + --hash=sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58 \ + --hash=sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5 \ + --hash=sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198 \ + --hash=sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536 \ + --hash=sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134 \ + --hash=sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf \ + --hash=sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2 \ + --hash=sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2 \ + --hash=sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370 \ + --hash=sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1 \ + --hash=sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154 \ + --hash=sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b \ + --hash=sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197 \ + --hash=sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386 \ + --hash=sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a \ + --hash=sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48 \ + --hash=sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748 \ + --hash=sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c \ + --hash=sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8 \ + --hash=sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5 \ + --hash=sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999 \ + --hash=sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369 \ + --hash=sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122 \ + --hash=sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b \ + --hash=sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098 \ + --hash=sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9 \ + --hash=sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f \ + --hash=sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799 \ + --hash=sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028 \ + --hash=sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2 \ + --hash=sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525 \ + --hash=sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d \ + --hash=sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb \ + --hash=sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872 \ + --hash=sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64 \ + --hash=sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586 \ + --hash=sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf \ + --hash=sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552 \ + --hash=sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2 \ + --hash=sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415 \ + --hash=sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c \ + --hash=sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6 \ + --hash=sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64 \ + --hash=sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d \ + --hash=sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548 \ + --hash=sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07 \ + --hash=sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61 \ + --hash=sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d \ + --hash=sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771 \ + --hash=sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9 \ + --hash=sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c \ + --hash=sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3 \ + --hash=sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16 \ + --hash=sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145 \ + --hash=sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611 \ + --hash=sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2 \ + --hash=sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464 \ + --hash=sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2 \ + --hash=sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04 \ + --hash=sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54 \ + --hash=sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df \ + --hash=sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f \ + --hash=sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1 \ + --hash=sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220 + # via matplotlib +matplotlib==3.10.5 \ + --hash=sha256:00b6feadc28a08bd3c65b2894f56cf3c94fc8f7adcbc6ab4516ae1e8ed8f62e2 \ + --hash=sha256:07442d2692c9bd1cceaa4afb4bbe5b57b98a7599de4dabfcca92d3eea70f9ebe \ + --hash=sha256:080c3676a56b8ee1c762bcf8fca3fe709daa1ee23e6ef06ad9f3fc17332f2d2a \ + --hash=sha256:160e125da27a749481eaddc0627962990f6029811dbeae23881833a011a0907f \ + --hash=sha256:1f5f3ec4c191253c5f2b7c07096a142c6a1c024d9f738247bfc8e3f9643fc975 \ + --hash=sha256:1fc0d2a3241cdcb9daaca279204a3351ce9df3c0e7e621c7e04ec28aaacaca30 \ + --hash=sha256:1ff10ea43288f0c8bab608a305dc6c918cc729d429c31dcbbecde3b9f4d5b569 \ + --hash=sha256:21a95b9bf408178d372814de7baacd61c712a62cae560b5e6f35d791776f6516 \ + --hash=sha256:27f52634315e96b1debbfdc5c416592edcd9c4221bc2f520fd39c33db5d9f202 \ + --hash=sha256:2efaf97d72629e74252e0b5e3c46813e9eeaa94e011ecf8084a971a31a97f40b \ + --hash=sha256:33775bbeb75528555a15ac29396940128ef5613cf9a2d31fb1bfd18b3c0c0903 \ + --hash=sha256:352ed6ccfb7998a00881692f38b4ca083c691d3e275b4145423704c34c909076 \ + --hash=sha256:354204db3f7d5caaa10e5de74549ef6a05a4550fdd1c8f831ab9bca81efd39ed \ + --hash=sha256:3967424121d3a46705c9fa9bdb0931de3228f13f73d7bb03c999c88343a89d89 \ + --hash=sha256:3b80eb8621331449fc519541a7461987f10afa4f9cfd91afcd2276ebe19bd56c \ + --hash=sha256:47a388908e469d6ca2a6015858fa924e0e8a2345a37125948d8e93a91c47933e \ + --hash=sha256:48fe6d47380b68a37ccfcc94f009530e84d41f71f5dae7eda7c4a5a84aa0a674 \ + --hash=sha256:4b4984d5064a35b6f66d2c11d668565f4389b1119cc64db7a4c1725bc11adffc \ + --hash=sha256:4fa40a8f98428f789a9dcacd625f59b7bc4e3ef6c8c7c80187a7a709475cf592 \ + --hash=sha256:525f6e28c485c769d1f07935b660c864de41c37fd716bfa64158ea646f7084bb \ + --hash=sha256:52c6573dfcb7726a9907b482cd5b92e6b5499b284ffacb04ffbfe06b3e568124 \ + --hash=sha256:56da3b102cf6da2776fef3e71cd96fcf22103a13594a18ac9a9b31314e0be154 \ + --hash=sha256:5d4773a6d1c106ca05cb5a5515d277a6bb96ed09e5c8fab6b7741b8fcaa62c8f \ + --hash=sha256:64c4535419d5617f7363dad171a5a59963308e0f3f813c4bed6c9e6e2c131512 \ + --hash=sha256:6c49465bf689c4d59d174d0c7795fb42a21d4244d11d70e52b8011987367ac61 \ + --hash=sha256:707f9c292c4cd4716f19ab8a1f93f26598222cd931e0cd98fbbb1c5994bf7667 \ + --hash=sha256:77fab633e94b9da60512d4fa0213daeb76d5a7b05156840c4fd0399b4b818837 \ + --hash=sha256:7e44cada61bec8833c106547786814dd4a266c1b2964fd25daa3804f1b8d4467 \ + --hash=sha256:8a8da0453a7fd8e3da114234ba70c5ba9ef0e98f190309ddfde0f089accd46ea \ + --hash=sha256:8b6b49167d208358983ce26e43aa4196073b4702858670f2eb111f9a10652b4b \ + --hash=sha256:8dee65cb1424b7dc982fe87895b5613d4e691cc57117e8af840da0148ca6c1d7 \ + --hash=sha256:903352681b59f3efbf4546985142a9686ea1d616bb054b09a537a06e4b892ccf \ + --hash=sha256:94986a242747a0605cb3ff1cb98691c736f28a59f8ffe5175acaeb7397c49a5a \ + --hash=sha256:95672a5d628b44207aab91ec20bf59c26da99de12b88f7e0b1fb0a84a86ff959 \ + --hash=sha256:96ef8f5a3696f20f55597ffa91c28e2e73088df25c555f8d4754931515512715 \ + --hash=sha256:97b9d6443419085950ee4a5b1ee08c363e5c43d7176e55513479e53669e88468 \ + --hash=sha256:a17e57e33de901d221a07af32c08870ed4528db0b6059dce7d7e65c1122d4bea \ + --hash=sha256:a23193db2e9d64ece69cac0c8231849db7dd77ce59c7b89948cf9d0ce655a3ce \ + --hash=sha256:a277033048ab22d34f88a3c5243938cef776493f6201a8742ed5f8b553201343 \ + --hash=sha256:a41bcb6e2c8e79dc99c5511ae6f7787d2fb52efd3d805fff06d5d4f667db16b2 \ + --hash=sha256:a6b310f95e1102a8c7c817ef17b60ee5d1851b8c71b63d9286b66b177963039e \ + --hash=sha256:ac3d50760394d78a3c9be6b28318fe22b494c4fcf6407e8fd4794b538251899b \ + --hash=sha256:b072aac0c3ad563a2b3318124756cb6112157017f7431626600ecbe890df57a1 \ + --hash=sha256:b5fa2e941f77eb579005fb804026f9d0a1082276118d01cc6051d0d9626eaa7f \ + --hash=sha256:ba6c3c9c067b83481d647af88b4e441d532acdb5ef22178a14935b0b881188f4 \ + --hash=sha256:c04cba0f93d40e45b3c187c6c52c17f24535b27d545f757a2fffebc06c12b98b \ + --hash=sha256:c61333a8e5e6240e73769d5826b9a31d8b22df76c0778f8480baf1b4b01c9420 \ + --hash=sha256:ceefe5d40807d29a66ae916c6a3915d60ef9f028ce1927b84e727be91d884369 \ + --hash=sha256:d52fd5b684d541b5a51fb276b2b97b010c75bee9aa392f96b4a07aeb491e33c7 \ + --hash=sha256:dc88af74e7ba27de6cbe6faee916024ea35d895ed3d61ef6f58c4ce97da7185a \ + --hash=sha256:dcfc39c452c6a9f9028d3e44d2d721484f665304857188124b505b2c95e1eecf \ + --hash=sha256:e4a6470a118a2e93022ecc7d3bd16b3114b2004ea2bf014fff875b3bc99b70c6 \ + --hash=sha256:ee7a09ae2f4676276f5a65bd9f2bd91b4f9fbaedf49f40267ce3f9b448de501f \ + --hash=sha256:ee98a5c5344dc7f48dc261b6ba5d9900c008fc12beb3fa6ebda81273602cc389 \ + --hash=sha256:f6adb644c9d040ffb0d3434e440490a66cf73dbfa118a6f79cd7568431f7a012 + # via sinter numpy==2.2.6 \ --hash=sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff \ --hash=sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47 \ @@ -64,11 +346,126 @@ numpy==2.2.6 \ --hash=sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249 \ --hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \ --hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8 - # via stim + # via + # contourpy + # matplotlib + # scipy + # sinter + # stim packaging==25.0 \ --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f - # via pytest + # via + # matplotlib + # pytest +pillow==11.3.0 \ + --hash=sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2 \ + --hash=sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214 \ + --hash=sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e \ + --hash=sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59 \ + --hash=sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50 \ + --hash=sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632 \ + --hash=sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06 \ + --hash=sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a \ + --hash=sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51 \ + --hash=sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced \ + --hash=sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f \ + --hash=sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12 \ + --hash=sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8 \ + --hash=sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6 \ + --hash=sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580 \ + --hash=sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f \ + --hash=sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac \ + --hash=sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860 \ + --hash=sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd \ + --hash=sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722 \ + --hash=sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8 \ + --hash=sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4 \ + --hash=sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673 \ + --hash=sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788 \ + --hash=sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542 \ + --hash=sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e \ + --hash=sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd \ + --hash=sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8 \ + --hash=sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523 \ + --hash=sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967 \ + --hash=sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809 \ + --hash=sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477 \ + --hash=sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027 \ + --hash=sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae \ + --hash=sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b \ + --hash=sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c \ + --hash=sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f \ + --hash=sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e \ + --hash=sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b \ + --hash=sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7 \ + --hash=sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27 \ + --hash=sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361 \ + --hash=sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae \ + --hash=sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d \ + --hash=sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc \ + --hash=sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58 \ + --hash=sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad \ + --hash=sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6 \ + --hash=sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024 \ + --hash=sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978 \ + --hash=sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb \ + --hash=sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d \ + --hash=sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0 \ + --hash=sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9 \ + --hash=sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f \ + --hash=sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874 \ + --hash=sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa \ + --hash=sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081 \ + --hash=sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149 \ + --hash=sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6 \ + --hash=sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d \ + --hash=sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd \ + --hash=sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f \ + --hash=sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c \ + --hash=sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31 \ + --hash=sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e \ + --hash=sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db \ + --hash=sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6 \ + --hash=sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f \ + --hash=sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494 \ + --hash=sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69 \ + --hash=sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94 \ + --hash=sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77 \ + --hash=sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d \ + --hash=sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7 \ + --hash=sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a \ + --hash=sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438 \ + --hash=sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288 \ + --hash=sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b \ + --hash=sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635 \ + --hash=sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3 \ + --hash=sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d \ + --hash=sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe \ + --hash=sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0 \ + --hash=sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe \ + --hash=sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a \ + --hash=sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805 \ + --hash=sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8 \ + --hash=sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36 \ + --hash=sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a \ + --hash=sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b \ + --hash=sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e \ + --hash=sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25 \ + --hash=sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12 \ + --hash=sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada \ + --hash=sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c \ + --hash=sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71 \ + --hash=sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d \ + --hash=sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c \ + --hash=sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6 \ + --hash=sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1 \ + --hash=sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50 \ + --hash=sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653 \ + --hash=sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c \ + --hash=sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4 \ + --hash=sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3 + # via matplotlib pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 @@ -77,10 +474,83 @@ pygments==2.19.1 \ --hash=sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f \ --hash=sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c # via pytest +pyparsing==3.2.3 \ + --hash=sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf \ + --hash=sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be + # via matplotlib pytest==8.4.0 \ --hash=sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6 \ --hash=sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e # via -r src/py/requirements.in +python-dateutil==2.9.0.post0 \ + --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ + --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 + # via matplotlib +scipy==1.16.1 \ + --hash=sha256:0851f6a1e537fe9399f35986897e395a1aa61c574b178c0d456be5b1a0f5ca1f \ + --hash=sha256:0a55ffe0ba0f59666e90951971a884d1ff6f4ec3275a48f472cfb64175570f77 \ + --hash=sha256:15240c3aac087a522b4eaedb09f0ad061753c5eebf1ea430859e5bf8640d5919 \ + --hash=sha256:18aca1646a29ee9a0625a1be5637fa798d4d81fdf426481f06d69af828f16958 \ + --hash=sha256:21a611ced9275cb861bacadbada0b8c0623bc00b05b09eb97f23b370fc2ae56d \ + --hash=sha256:226652fca853008119c03a8ce71ffe1b3f6d2844cc1686e8f9806edafae68596 \ + --hash=sha256:2ef500e72f9623a6735769e4b93e9dcb158d40752cdbb077f305487e3e2d1f45 \ + --hash=sha256:30cc4bb81c41831ecfd6dc450baf48ffd80ef5aed0f5cf3ea775740e80f16ecc \ + --hash=sha256:367d567ee9fc1e9e2047d31f39d9d6a7a04e0710c86e701e053f237d14a9b4f6 \ + --hash=sha256:3d0b80fb26d3e13a794c71d4b837e2a589d839fd574a6bbb4ee1288c213ad4a3 \ + --hash=sha256:3ddfb1e8d0b540cb4ee9c53fc3dea3186f97711248fb94b4142a1b27178d8b4b \ + --hash=sha256:3ea0733a2ff73fd6fdc5fecca54ee9b459f4d74f00b99aced7d9a3adb43fb1cc \ + --hash=sha256:44c76f9e8b6e8e488a586190ab38016e4ed2f8a038af7cd3defa903c0a2238b3 \ + --hash=sha256:4cf5785e44e19dcd32a0e4807555e1e9a9b8d475c6afff3d21c3c543a6aa84f4 \ + --hash=sha256:4dc0e7be79e95d8ba3435d193e0d8ce372f47f774cffd882f88ea4e1e1ddc731 \ + --hash=sha256:5451606823a5e73dfa621a89948096c6528e2896e40b39248295d3a0138d594f \ + --hash=sha256:57d75524cb1c5a374958a2eae3d84e1929bb971204cc9d52213fb8589183fc19 \ + --hash=sha256:5aa2687b9935da3ed89c5dbed5234576589dd28d0bf7cd237501ccfbdf1ad608 \ + --hash=sha256:5e1a106f8c023d57a2a903e771228bf5c5b27b5d692088f457acacd3b54511e4 \ + --hash=sha256:65f81a25805f3659b48126b5053d9e823d3215e4a63730b5e1671852a1705921 \ + --hash=sha256:6c62eea7f607f122069b9bad3f99489ddca1a5173bef8a0c75555d7488b6f725 \ + --hash=sha256:6e5c2f74e5df33479b5cd4e97a9104c511518fbd979aa9b8f6aec18b2e9ecae7 \ + --hash=sha256:709559a1db68a9abc3b2c8672c4badf1614f3b440b3ab326d86a5c0491eafae3 \ + --hash=sha256:744d977daa4becb9fc59135e75c069f8d301a87d64f88f1e602a9ecf51e77b27 \ + --hash=sha256:796a5a9ad36fa3a782375db8f4241ab02a091308eb079746bc0f874c9b998318 \ + --hash=sha256:81929ed0fa7a5713fcdd8b2e6f73697d3b4c4816d090dd34ff937c20fa90e8ab \ + --hash=sha256:81b433bbeaf35728dad619afc002db9b189e45eebe2cd676effe1fb93fef2b9c \ + --hash=sha256:8503517c44c18d1030d666cb70aaac1cc8913608816e06742498833b128488b7 \ + --hash=sha256:85764fb15a2ad994e708258bb4ed8290d1305c62a4e1ef07c414356a24fcfbf8 \ + --hash=sha256:886cc81fdb4c6903a3bb0464047c25a6d1016fef77bb97949817d0c0d79f9e04 \ + --hash=sha256:89728678c5ca5abd610aee148c199ac1afb16e19844401ca97d43dc548a354eb \ + --hash=sha256:8dfbb25dffc4c3dd9371d8ab456ca81beeaf6f9e1c2119f179392f0dc1ab7695 \ + --hash=sha256:978d8311674b05a8f7ff2ea6c6bce5d8b45a0cb09d4c5793e0318f448613ea65 \ + --hash=sha256:adccd93a2fa937a27aae826d33e3bfa5edf9aa672376a4852d23a7cd67a2e5b7 \ + --hash=sha256:bcc12db731858abda693cecdb3bdc9e6d4bd200213f49d224fe22df82687bdd6 \ + --hash=sha256:c033fa32bab91dc98ca59d0cf23bb876454e2bb02cbe592d5023138778f70030 \ + --hash=sha256:c0c804d60492a0aad7f5b2bb1862f4548b990049e27e828391ff2bf6f7199998 \ + --hash=sha256:c24fa02f7ed23ae514460a22c57eca8f530dbfa50b1cfdbf4f37c05b5309cc39 \ + --hash=sha256:ca66d980469cb623b1759bdd6e9fd97d4e33a9fad5b33771ced24d0cb24df67e \ + --hash=sha256:cb18899127278058bcc09e7b9966d41a5a43740b5bb8dcba401bd983f82e885b \ + --hash=sha256:cc1d2f2fd48ba1e0620554fe5bc44d3e8f5d4185c8c109c7fbdf5af2792cfad2 \ + --hash=sha256:d85495cef541729a70cdddbbf3e6b903421bc1af3e8e3a9a72a06751f33b7c39 \ + --hash=sha256:d8da7c3dd67bcd93f15618938f43ed0995982eb38973023d46d4646c4283ad65 \ + --hash=sha256:dc54f76ac18073bcecffb98d93f03ed6b81a92ef91b5d3b135dcc81d55a724c7 \ + --hash=sha256:e756d688cb03fd07de0fffad475649b03cb89bee696c98ce508b17c11a03f95c \ + --hash=sha256:e7cc1ffcc230f568549fc56670bcf3df1884c30bd652c5da8138199c8c76dae0 \ + --hash=sha256:e8fd15fc5085ab4cca74cb91fe0a4263b1f32e4420761ddae531ad60934c2119 \ + --hash=sha256:f006e323874ffd0b0b816d8c6a8e7f9a73d55ab3b8c3f72b752b226d0e3ac83d \ + --hash=sha256:f0ebb7204f063fad87fc0a0e4ff4a2ff40b2a226e4ba1b7e34bf4b79bf97cd86 \ + --hash=sha256:f1b9e5962656f2734c2b285a8745358ecb4e4efbadd00208c80a389227ec61ff \ + --hash=sha256:f23634f9e5adb51b2a77766dac217063e764337fbc816aa8ad9aaebcd4397fd3 \ + --hash=sha256:f7b8013c6c066609577d910d1a2a077021727af07b6fab0ee22c2f901f22352a \ + --hash=sha256:f8a5d6cd147acecc2603fbd382fed6c46f474cccfcf69ea32582e033fb54dcfe \ + --hash=sha256:f965bbf3235b01c776115ab18f092a95aa74c271a52577bcb0563e85738fd618 \ + --hash=sha256:fedc2cbd1baed37474b1924c331b97bdff611d762c196fac1a9b71e67b813b1b + # via sinter +sinter==1.15.0 \ + --hash=sha256:47c7e4412d73bcbd5cabca49b6d9bfc0b33eb18d333f525076512e0efa873d45 \ + --hash=sha256:6bec05cc643e301192c2acaca9ede464576572635c5e25a592e48294b7e53bed + # via -r src/py/requirements.in +six==1.17.0 \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ + --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 + # via python-dateutil stim==1.15.0 \ --hash=sha256:0bb3757c69c9b16fd24ff7400b5cddb22017c4cae84fc4b7b73f84373cb03c00 \ --hash=sha256:190c5a3c9cecdfae3302d02057d1ed6d9ce7910d2bcc2ff375807d8f8ec5494d \ @@ -116,4 +586,6 @@ stim==1.15.0 \ --hash=sha256:fb9465ab120837ecbd26b5af216a00715f04da087ddcfa09646892c8de720d09 \ --hash=sha256:fc613f78bc88b4318d7f34f9fddacec52638c11b72cc618f911bdd7ca153f938 \ --hash=sha256:fdd9e5ab85ba2fb113b8834422518f6e46a4aea2e0f6f7305cfc2ad0fcd07086 - # via -r src/py/requirements.in + # via + # -r src/py/requirements.in + # sinter diff --git a/src/py/tesseract_sinter_compat_test.py b/src/py/tesseract_sinter_compat_test.py index 7cafe1e8..2f806309 100644 --- a/src/py/tesseract_sinter_compat_test.py +++ b/src/py/tesseract_sinter_compat_test.py @@ -17,8 +17,10 @@ import numpy as np import stim import shutil +from sinter._decoding._decoding import sample_decode -from src import tesseract_sinter_compat as tesseract_module +from src.tesseract_decoder import tesseract_sinter_compat as tesseract_module +from src import tesseract_decoder def test_tesseract_sinter_obj_exists(): @@ -30,11 +32,12 @@ def test_tesseract_sinter_obj_exists(): assert hasattr(decoder, 'compile_decoder_for_dem') assert hasattr(decoder, 'decode_via_files') -def test_compile_decoder_for_dem(): +@pytest.mark.parametrize("use_custom_config", [False, True]) +def test_compile_decoder_for_dem(use_custom_config): """ - Test the 'compile_decoder_for_dem' method with a specific DEM. + Test the 'compile_decoder_for_dem' method with and without a custom config. """ - + dem = stim.DetectorErrorModel(""" detector(0, 0, 0) D0 detector(0, 0, 1) D1 @@ -44,17 +47,26 @@ def test_compile_decoder_for_dem(): error(0.1) D1 D2 L1 error(0.1) D2 D3 L0 """) + + if use_custom_config: + config = tesseract_decoder.tesseract.TesseractConfig() + config.verbose = True + decoder = tesseract_module.TesseractSinterDecoder(config=config) + else: + decoder = tesseract_module.TesseractSinterDecoder() - decoder = tesseract_module.TesseractSinterDecoder() compiled_decoder = decoder.compile_decoder_for_dem(dem=dem) - + assert compiled_decoder is not None assert hasattr(compiled_decoder, 'decode_shots_bit_packed') - + # Verify the detector and observable counts are correct assert compiled_decoder.num_detectors == dem.num_detectors assert compiled_decoder.num_observables == dem.num_observables + # Verify the config was correctly applied + assert compiled_decoder.decoder.config.verbose == use_custom_config + def test_decode_shots_bit_packed(): """ Tests the 'decode_shots_bit_packed' method with a specific DEM and detection event. @@ -176,7 +188,8 @@ def test_decode_via_files_sanity_check(): if temp_dir.exists(): shutil.rmtree(temp_dir) -def test_decode_via_files(): +@pytest.mark.parametrize("use_custom_config", [False, True]) +def test_decode_via_files(use_custom_config): """ Tests the 'decode_via_files' method with a specific DEM and detection event. """ @@ -222,7 +235,14 @@ def test_decode_via_files(): with open(dets_in_path, 'wb') as f: f.write(detection_events_np.tobytes()) - tesseract_module.TesseractSinterDecoder().decode_via_files( + if use_custom_config: + config = tesseract_decoder.tesseract.TesseractConfig() + config.verbose = True + decoder = tesseract_module.TesseractSinterDecoder(config=config) + else: + decoder = tesseract_module.TesseractSinterDecoder() + + decoder.decode_via_files( num_shots=num_shots, num_dets=num_detectors, num_obs=dem.num_observables, @@ -248,6 +268,9 @@ def test_decode_via_files(): # Clean up temporary files if temp_dir.exists(): shutil.rmtree(temp_dir) + + assert decoder.config.verbose == use_custom_config + def test_decode_via_files_multi_shot(): """ @@ -325,5 +348,152 @@ def test_decode_via_files_multi_shot(): if temp_dir.exists(): shutil.rmtree(temp_dir) +def construct_tesseract_decoder_for_sinter(): + return {"tesseract": tesseract_module.TesseractSinterDecoder()} + + +def test_sinter_decode_repetition_code(): + """ + Tests the 'tesseract' decoder on a repetition code circuit. + """ + circuit = stim.Circuit.generated('repetition_code:memory', + rounds=3, + distance=3, + 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, + num_shots=1000, + decoder="tesseract", + custom_decoders=construct_tesseract_decoder_for_sinter(), + ) + assert result.discards == 0 + assert 0 <= result.errors <= 100 + assert result.shots == 1000 + + +def test_sinter_decode_surface_code(): + """ + Tests the 'tesseract' decoder on a more complex surface code circuit. + """ + circuit = stim.Circuit.generated( + "surface_code:rotated_memory_x", + distance=3, + rounds=15, + after_clifford_depolarization=0.001, + ) + result = sample_decode( + 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=construct_tesseract_decoder_for_sinter(), + ) + assert result.discards == 0 + assert 0 <= result.errors <= 50 + assert result.shots == 1000 + +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, + num_shots=1000, + decoder="tesseract", + custom_decoders=construct_tesseract_decoder_for_sinter(), + ) + assert result.discards == 0 + assert result.shots == 1000 + assert result.errors == 0 + +def test_sinter_no_observables(): + """ + Tests the decoder on a circuit with detectors but no logical observables. + """ + circuit = stim.Circuit(""" + X_ERROR(0.1) 0 + 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, + num_shots=1000, + decoder="tesseract", + custom_decoders=construct_tesseract_decoder_for_sinter(), + ) + assert result.discards == 0 + assert result.shots == 1000 + assert result.errors == 0 + +def test_sinter_invincible_observables(): + """ + Tests the decoder on a circuit where an observable is not affected by errors. + """ + circuit = stim.Circuit(""" + X_ERROR(0.1) 0 + M 0 1 + 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, + num_shots=1000, + decoder="tesseract", + custom_decoders=construct_tesseract_decoder_for_sinter(), + ) + assert result.discards == 0 + assert result.shots == 1000 + assert result.errors == 0 + + + +def test_sinter_detector_counting(): + """ + Tests 'that the decoder's detector count is correctly reported via Sinter'. + """ + circuit = stim.Circuit(""" + X_ERROR(0.1) 0 + X_ERROR(0.2) 1 + M 0 1 + DETECTOR rec[-1] + DETECTOR rec[-2] + 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, + num_shots=10000, + decoder="tesseract", + count_detection_events=True, + custom_decoders=construct_tesseract_decoder_for_sinter(), + ) + assert result.discards == 0 + assert result.custom_counts['detectors_checked'] == 20000 + assert 0.3 * 10000 * 0.5 <= result.custom_counts['detection_events'] <= 0.3 * 10000 * 2.0 + assert set(result.custom_counts.keys()) == {'detectors_checked', 'detection_events'} + + + + if __name__ == "__main__": raise SystemExit(pytest.main([__file__])) \ No newline at end of file diff --git a/src/tesseract.pybind.cc b/src/tesseract.pybind.cc index 6342d6e0..7838343c 100644 --- a/src/tesseract.pybind.cc +++ b/src/tesseract.pybind.cc @@ -20,6 +20,7 @@ #include "common.pybind.h" #include "pybind11/detail/common.h" #include "simplex.pybind.h" +#include "tesseract_sinter_compat.pybind.h" #include "utils.pybind.h" #include "visualization.pybind.h" @@ -31,6 +32,7 @@ PYBIND11_MODULE(tesseract_decoder, tesseract) { add_simplex_module(tesseract); add_tesseract_module(tesseract); add_visualization_module(tesseract); + pybind_sinter_compat(tesseract); // Adds a context manager to the python library that can be used to redirect C++'s stdout/stderr // to python's stdout/stderr at run time like diff --git a/src/tesseract.pybind.h b/src/tesseract.pybind.h index 74e90275..393883fd 100644 --- a/src/tesseract.pybind.h +++ b/src/tesseract.pybind.h @@ -56,6 +56,10 @@ void add_tesseract_module(py::module& root) { This class holds all the parameters needed to initialize and configure a Tesseract decoder instance. + )pbdoc") + .def(py::init<>(), R"pbdoc( + Default constructor for TesseractConfig. + Creates a new instance with default parameter values. )pbdoc") .def(py::init(&tesseract_config_maker), py::arg("dem"), py::arg("det_beam") = INF_DET_BEAM, py::arg("beam_climbing") = false, py::arg("no_revisit_dets") = false, diff --git a/src/tesseract_sinter_compat.pybind.cc b/src/tesseract_sinter_compat.pybind.h similarity index 88% rename from src/tesseract_sinter_compat.pybind.cc rename to src/tesseract_sinter_compat.pybind.h index 86585445..d5413148 100644 --- a/src/tesseract_sinter_compat.pybind.cc +++ b/src/tesseract_sinter_compat.pybind.h @@ -100,7 +100,14 @@ struct TesseractSinterCompiledDecoder { // a decoder for a specific Detector Error Model (DEM). //-------------------------------------------------------------------------------------------------- struct TesseractSinterDecoder { - TesseractSinterDecoder() {} + // Use TesseractConfig as an integrated property. + TesseractConfig config; + + // Default constructor + TesseractSinterDecoder() : config(TesseractConfig()) {} + + // Constructor with TesseractConfig parameter + TesseractSinterDecoder(const TesseractConfig& config_in) : config(config_in) {} bool operator==(const TesseractSinterDecoder& other) const { return true; @@ -114,10 +121,9 @@ struct TesseractSinterDecoder { TesseractSinterCompiledDecoder compile_decoder_for_dem(const py::object& dem) { const stim::DetectorErrorModel stim_dem(py::cast(py::str(dem)).c_str()); - TesseractConfig config; - config.dem = stim_dem; - - auto decoder = std::make_unique(config); + TesseractConfig local_config = config; + local_config.dem = stim_dem; + auto decoder = std::make_unique(local_config); return TesseractSinterCompiledDecoder{ .decoder = std::move(decoder), @@ -145,10 +151,10 @@ struct TesseractSinterDecoder { dem_file.close(); // Construct TesseractDecoder. + TesseractConfig local_config = config; const stim::DetectorErrorModel stim_dem(dem_content_str.c_str()); - TesseractConfig config; - config.dem = stim_dem; - TesseractDecoder decoder(config); + local_config.dem = stim_dem; + TesseractDecoder decoder(local_config); // Calculate expected number of bytes per shot for detectors and observables. const uint64_t num_detector_bytes = (num_dets + 7) / 8; @@ -203,11 +209,11 @@ struct TesseractSinterDecoder { //-------------------------------------------------------------------------------------------------- // Expose C++ classes to the Python interpreter. //-------------------------------------------------------------------------------------------------- -void pybind_sinter_compat(py::module& m) { - m.doc() = R"pbdoc( +void pybind_sinter_compat(py::module& root) { + auto m = root.def_submodule("tesseract_sinter_compat", R"pbdoc( This module provides Python bindings for the Tesseract quantum error correction decoder, designed for compatibility with the Sinter library. - )pbdoc"; + )pbdoc"); // Bind the TesseractSinterCompiledDecoder. py::class_(m, "TesseractSinterCompiledDecoder", R"pbdoc( @@ -232,13 +238,30 @@ void pybind_sinter_compat(py::module& m) { R"pbdoc(The number of detectors in the decoder's underlying DEM.)pbdoc") .def_readwrite( "num_observables", &TesseractSinterCompiledDecoder::num_observables, - R"pbdoc(The number of logical observables in the decoder's underlying DEM.)pbdoc"); + R"pbdoc(The number of logical observables in the decoder's underlying DEM.)pbdoc") + .def_property_readonly( + "decoder", + [](const TesseractSinterCompiledDecoder& self) -> const TesseractDecoder& { + return *self.decoder; + }, + py::return_value_policy::reference_internal, + R"pbdoc(The internal TesseractDecoder instance.)pbdoc"); // Bind the TesseractSinterDecoder. py::class_(m, "TesseractSinterDecoder", R"pbdoc( A factory for creating Tesseract decoders compatible with `sinter`. )pbdoc") - .def(py::init<>(), R"pbdoc(Initializes a new TesseractSinterDecoder instance.)pbdoc") + .def(py::init<>(), R"pbdoc( + Initializes a new TesseractSinterDecoder instance with a default TesseractConfig. + )pbdoc") + .def(py::init(), py::kw_only(), py::arg("config"), + R"pbdoc( + Initializes a new TesseractSinterDecoder instance with a custom TesseractConfig object. + + :param config: A `TesseractConfig` object to configure the decoder. + )pbdoc") + .def_readwrite("config", &TesseractSinterDecoder::config, + R"pbdoc(The TesseractConfig object for the decoder.)pbdoc") .def("compile_decoder_for_dem", &TesseractSinterDecoder::compile_decoder_for_dem, py::kw_only(), py::arg("dem"), R"pbdoc( @@ -267,26 +290,4 @@ void pybind_sinter_compat(py::module& m) { R"pbdoc(Checks if two TesseractSinterDecoder instances are equal.)pbdoc") .def(py::self != py::self, R"pbdoc(Checks if two TesseractSinterDecoder instances are not equal.)pbdoc"); - - // Define the sinter_decoders function, which is the entry point for sinter. - m.def( - "sinter_decoders", - []() { - py::dict result; - result["tesseract"] = TesseractSinterDecoder(); - return result; - }, - R"pbdoc( - Returns a dictionary of available Sinter-compatible decoders. - - This function is the main entry point for the Sinter library's custom decoder - integration. It returns a dictionary where keys are decoder names (e.g., "tesseract") - and values are corresponding `sinter.Decoder` objects. - )pbdoc"); -} - -// Node: I need this macro, which will create the entry point function that -// Python interpreter call when I import this module. -PYBIND11_MODULE(tesseract_sinter_compat, m) { - pybind_sinter_compat(m); } From 9c4a6f18d2ed2dfca6f52b76550a86c99d72abf7 Mon Sep 17 00:00:00 2001 From: Dragana Grbic Date: Tue, 12 Aug 2025 13:32:27 -0700 Subject: [PATCH 05/14] Make sinter decoder pickable --- src/py/tesseract_sinter_compat_test.py | 13 ++++++++++++ src/tesseract_sinter_compat.pybind.h | 28 +++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/py/tesseract_sinter_compat_test.py b/src/py/tesseract_sinter_compat_test.py index 2f806309..5db514fe 100644 --- a/src/py/tesseract_sinter_compat_test.py +++ b/src/py/tesseract_sinter_compat_test.py @@ -21,6 +21,7 @@ from src.tesseract_decoder import tesseract_sinter_compat as tesseract_module from src import tesseract_decoder +import sinter def test_tesseract_sinter_obj_exists(): @@ -493,6 +494,18 @@ def test_sinter_detector_counting(): assert set(result.custom_counts.keys()) == {'detectors_checked', 'detection_events'} +def test_full_scale(): + result, = sinter.collect( + num_workers=2, + tasks=[sinter.Task(circuit=stim.Circuit())], + decoders=["tesseract"], + max_shots=1000, + custom_decoders=construct_tesseract_decoder_for_sinter(), + ) + assert result.discards == 0 + assert result.shots == 1000 + assert result.errors == 0 + if __name__ == "__main__": diff --git a/src/tesseract_sinter_compat.pybind.h b/src/tesseract_sinter_compat.pybind.h index d5413148..6cffe972 100644 --- a/src/tesseract_sinter_compat.pybind.h +++ b/src/tesseract_sinter_compat.pybind.h @@ -289,5 +289,31 @@ void pybind_sinter_compat(py::module& root) { .def(py::self == py::self, R"pbdoc(Checks if two TesseractSinterDecoder instances are equal.)pbdoc") .def(py::self != py::self, - R"pbdoc(Checks if two TesseractSinterDecoder instances are not equal.)pbdoc"); + R"pbdoc(Checks if two TesseractSinterDecoder instances are not equal.)pbdoc") + .def(py::pickle( + [](const TesseractSinterDecoder& self) -> py::tuple { // __getstate__ + return py::make_tuple( + std::string(self.config.dem.str()), self.config.det_beam, self.config.beam_climbing, + self.config.no_revisit_dets, self.config.at_most_two_errors_per_detector, + self.config.verbose, self.config.merge_errors, self.config.pqlimit, + self.config.det_orders, self.config.det_penalty, self.config.create_visualization); + }, + [](py::tuple t) { // __setstate__ + if (t.size() != 11) { + throw std::runtime_error("Invalid state for TesseractSinterDecoder!"); + } + TesseractConfig config; + config.dem = stim::DetectorErrorModel(t[0].cast()); + config.det_beam = t[1].cast(); + config.beam_climbing = t[2].cast(); + config.no_revisit_dets = t[3].cast(); + config.at_most_two_errors_per_detector = t[4].cast(); + config.verbose = t[5].cast(); + config.merge_errors = t[6].cast(); + config.pqlimit = t[7].cast(); + config.det_orders = t[8].cast>>(); + config.det_penalty = t[9].cast(); + config.create_visualization = t[10].cast(); + return TesseractSinterDecoder(config); + })); } From e88f9c7e00ba2411ff17f921cadac80dc0962ea4 Mon Sep 17 00:00:00 2001 From: Dragana Grbic Date: Tue, 12 Aug 2025 14:07:02 -0700 Subject: [PATCH 06/14] Documentation for Sinter integration --- src/py/README.md | 88 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/src/py/README.md b/src/py/README.md index 79c0c396..366ce197 100644 --- a/src/py/README.md +++ b/src/py/README.md @@ -481,3 +481,91 @@ print("\nEstimated DEM:") print(estimated_dem) # Expected probabilities: D0 -> 100/1000 = 0.1, D1 -> 250/1000 = 0.25, D2 -> 40/1000 = 0.04 ``` + +### 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 TesseractSinterDecoder Object +All Sinter examples rely on this utility function to provide the Sinter-compatible Tesseract decoder. + +```python +import sinter +import stim +from sinter._decoding._decoding import sample_decode + +from src.tesseract_decoder import tesseract_sinter_compat as tesseract_module +from src import tesseract_decoder + +# Define a function that returns a dictionary mapping a decoder name to its +# Sinter-compatible decoder object. +def get_tesseract_decoder_for_sinter(): + return {"tesseract": tesseract_module.TesseractSinterDecoder()} +``` + +#### Decoding with `sinter.collect` +`sinter.collect` is a powerful function for running many decoding jobs in parallel and collecting the results for large-scale benchmarking. + +```python +# Create a repetition code circuit to test the decoder. +circuit = stim.Circuit.generated( + 'repetition_code:memory', + distance=3, + rounds=3, + after_clifford_depolarization=0.01 +) + +# Use sinter.collect to run the decoding task. +results, = sinter.collect( + num_workers=1, + tasks=[sinter.Task(circuit=circuit)], + decoders=["tesseract"], + max_shots=1000, + custom_decoders=get_tesseract_decoder_for_sinter(), +) + +# Print a summary of the decoding results. +print("Basic Repetition Code Decoding Results:") +print(f"Shots run: {results.shots}") +print(f"Observed errors: {results.errors}") +print(f"Logical error rate: {results.errors / results.shots}") +``` + +#### Running with multiple workers +This example demonstrates how to use multiple worker threads to speed up the simulation. +```python +# Use sinter.collect with multiple workers for faster decoding. +results, = sinter.collect( + num_workers=4, + tasks=[sinter.Task(circuit=circuit)], + decoders=["tesseract"], + max_shots=10000, + custom_decoders=get_tesseract_decoder_for_sinter(), +) + +print("\nDecoding with 4 worker threads:") +print(f"Shots run: {results.shots}") +print(f"Observed errors: {results.errors}") +print(f"Logical error rate: {results.errors / results.shots}") +``` + +#### Decoding with `sinter.sample_decode` +`sinter.sample_decode` is a simpler, non-parallel function for directly decoding a single circuit. It's useful for quick tests and debugging without the overhead of the `sinter.collect` framework. + +```python +# Create a repetition code circuit. +circuit = stim.Circuit.generated('repetition_code:memory', distance=5, rounds=5) + +# Use sinter.sample_decode for a direct decoding run. +result = sample_decode( + circuit_obj=circuit, + dem_obj=circuit.detector_error_model(), + num_shots=1000, + decoder="tesseract", + custom_decoders=get_tesseract_decoder_for_sinter(), +) + +print("Basic sample_decode Results:") +print(f"Shots run: {result.shots}") +print(f"Observed errors: {result.errors}") +print(f"Logical error rate: {result.errors / result.shots}") +``` \ No newline at end of file From 839d4a93ce6473c007c1bcba6938dfa4be152267 Mon Sep 17 00:00:00 2001 From: Dragana Grbic Date: Wed, 13 Aug 2025 12:45:55 -0700 Subject: [PATCH 07/14] Add Oscar's test --- src/py/tesseract_sinter_compat_test.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/py/tesseract_sinter_compat_test.py b/src/py/tesseract_sinter_compat_test.py index 5db514fe..186691db 100644 --- a/src/py/tesseract_sinter_compat_test.py +++ b/src/py/tesseract_sinter_compat_test.py @@ -506,7 +506,29 @@ def test_full_scale(): assert result.shots == 1000 assert result.errors == 0 +def test_full_scale_one_worker(): + # Create a repetition code circuit to test the decoder. + circuit = stim.Circuit.generated( + 'repetition_code:memory', + distance=3, + rounds=3, + after_clifford_depolarization=0.01 + ) + + # Use sinter.collect to run the decoding task. + results, = sinter.collect( + num_workers=1, + tasks=[sinter.Task(circuit=circuit)], + decoders=["tesseract"], + max_shots=1000, + custom_decoders=construct_tesseract_decoder_for_sinter(), + ) + # Print a summary of the decoding results. + print("Basic Repetition Code Decoding Results:") + print(f"Shots run: {results.shots}") + print(f"Observed errors: {results.errors}") + print(f"Logical error rate: {results.errors / results.shots}") if __name__ == "__main__": raise SystemExit(pytest.main([__file__])) \ No newline at end of file From 4449c43ca53a5800b5a284514a08f95a5221c738 Mon Sep 17 00:00:00 2001 From: Dragana Grbic Date: Wed, 13 Aug 2025 13:43:08 -0700 Subject: [PATCH 08/14] Remove print statements inside sinter test --- src/py/tesseract_sinter_compat_test.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/py/tesseract_sinter_compat_test.py b/src/py/tesseract_sinter_compat_test.py index 186691db..a405329c 100644 --- a/src/py/tesseract_sinter_compat_test.py +++ b/src/py/tesseract_sinter_compat_test.py @@ -516,7 +516,7 @@ def test_full_scale_one_worker(): ) # Use sinter.collect to run the decoding task. - results, = sinter.collect( + result, = sinter.collect( num_workers=1, tasks=[sinter.Task(circuit=circuit)], decoders=["tesseract"], @@ -524,11 +524,8 @@ def test_full_scale_one_worker(): custom_decoders=construct_tesseract_decoder_for_sinter(), ) - # Print a summary of the decoding results. - print("Basic Repetition Code Decoding Results:") - print(f"Shots run: {results.shots}") - print(f"Observed errors: {results.errors}") - print(f"Logical error rate: {results.errors / results.shots}") + assert result.discards == 0 + assert result.shots == 1000 if __name__ == "__main__": raise SystemExit(pytest.main([__file__])) \ No newline at end of file From cd395c9e879c7b8f9632ab65ee9ea9d8cfd32ce7 Mon Sep 17 00:00:00 2001 From: Dragana Grbic Date: Wed, 13 Aug 2025 14:47:06 -0700 Subject: [PATCH 09/14] Add custom decoders dict --- src/tesseract_sinter_compat.pybind.h | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/tesseract_sinter_compat.pybind.h b/src/tesseract_sinter_compat.pybind.h index 6cffe972..3d15e6b2 100644 --- a/src/tesseract_sinter_compat.pybind.h +++ b/src/tesseract_sinter_compat.pybind.h @@ -316,4 +316,17 @@ void pybind_sinter_compat(py::module& root) { config.create_visualization = t[10].cast(); return TesseractSinterDecoder(config); })); + + // Add a function to create a dictionary of custom decoders + m.def( + "make_tesseract_sinter_decoders_dict", + []() -> py::object { + auto result = py::dict(); + result["tesseract"] = TesseractSinterDecoder{}; + return result; + }, + R"pbdoc( + Returns a dictionary mapping decoder names to sinter.Decoder-style objects. + This allows Sinter to easily discover and use Tesseract as a custom decoder. + )pbdoc"); } From f37f370d21156889a23cff670a7e173db288e567 Mon Sep 17 00:00:00 2001 From: Dragana Grbic Date: Wed, 13 Aug 2025 14:55:33 -0700 Subject: [PATCH 10/14] Update sinter tests with custom detectors dict --- src/py/README.md | 2 +- src/py/tesseract_sinter_compat_test.py | 18 ++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/py/README.md b/src/py/README.md index 366ce197..295fece2 100644 --- a/src/py/README.md +++ b/src/py/README.md @@ -499,7 +499,7 @@ from src import tesseract_decoder # Define a function that returns a dictionary mapping a decoder name to its # Sinter-compatible decoder object. def get_tesseract_decoder_for_sinter(): - return {"tesseract": tesseract_module.TesseractSinterDecoder()} + return tesseract_module.make_tesseract_sinter_decoders_dict() ``` #### Decoding with `sinter.collect` diff --git a/src/py/tesseract_sinter_compat_test.py b/src/py/tesseract_sinter_compat_test.py index a405329c..0ac17f7d 100644 --- a/src/py/tesseract_sinter_compat_test.py +++ b/src/py/tesseract_sinter_compat_test.py @@ -349,8 +349,6 @@ def test_decode_via_files_multi_shot(): if temp_dir.exists(): shutil.rmtree(temp_dir) -def construct_tesseract_decoder_for_sinter(): - return {"tesseract": tesseract_module.TesseractSinterDecoder()} def test_sinter_decode_repetition_code(): @@ -369,7 +367,7 @@ def test_sinter_decode_repetition_code(): dem_path=None, num_shots=1000, decoder="tesseract", - custom_decoders=construct_tesseract_decoder_for_sinter(), + custom_decoders=tesseract_module.make_tesseract_sinter_decoders_dict(), ) assert result.discards == 0 assert 0 <= result.errors <= 100 @@ -393,7 +391,7 @@ def test_sinter_decode_surface_code(): dem_obj=circuit.detector_error_model(decompose_errors=True), dem_path=None, decoder="tesseract", - custom_decoders=construct_tesseract_decoder_for_sinter(), + custom_decoders=tesseract_module.make_tesseract_sinter_decoders_dict(), ) assert result.discards == 0 assert 0 <= result.errors <= 50 @@ -411,7 +409,7 @@ def test_sinter_empty(): dem_path=None, num_shots=1000, decoder="tesseract", - custom_decoders=construct_tesseract_decoder_for_sinter(), + custom_decoders=tesseract_module.make_tesseract_sinter_decoders_dict(), ) assert result.discards == 0 assert result.shots == 1000 @@ -433,7 +431,7 @@ def test_sinter_no_observables(): dem_path=None, num_shots=1000, decoder="tesseract", - custom_decoders=construct_tesseract_decoder_for_sinter(), + custom_decoders=tesseract_module.make_tesseract_sinter_decoders_dict(), ) assert result.discards == 0 assert result.shots == 1000 @@ -456,7 +454,7 @@ def test_sinter_invincible_observables(): dem_path=None, num_shots=1000, decoder="tesseract", - custom_decoders=construct_tesseract_decoder_for_sinter(), + custom_decoders=tesseract_module.make_tesseract_sinter_decoders_dict(), ) assert result.discards == 0 assert result.shots == 1000 @@ -486,7 +484,7 @@ def test_sinter_detector_counting(): num_shots=10000, decoder="tesseract", count_detection_events=True, - custom_decoders=construct_tesseract_decoder_for_sinter(), + custom_decoders=tesseract_module.make_tesseract_sinter_decoders_dict(), ) assert result.discards == 0 assert result.custom_counts['detectors_checked'] == 20000 @@ -500,7 +498,7 @@ def test_full_scale(): tasks=[sinter.Task(circuit=stim.Circuit())], decoders=["tesseract"], max_shots=1000, - custom_decoders=construct_tesseract_decoder_for_sinter(), + custom_decoders=tesseract_module.make_tesseract_sinter_decoders_dict(), ) assert result.discards == 0 assert result.shots == 1000 @@ -521,7 +519,7 @@ def test_full_scale_one_worker(): tasks=[sinter.Task(circuit=circuit)], decoders=["tesseract"], max_shots=1000, - custom_decoders=construct_tesseract_decoder_for_sinter(), + custom_decoders=tesseract_module.make_tesseract_sinter_decoders_dict(), ) assert result.discards == 0 From 1c9f5aa9240835e654f87e44ba625a81d6088551 Mon Sep 17 00:00:00 2001 From: Dragana Grbic Date: Wed, 13 Aug 2025 15:31:55 -0700 Subject: [PATCH 11/14] Another sinter test --- src/py/tesseract_sinter_compat_test.py | 60 ++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/py/tesseract_sinter_compat_test.py b/src/py/tesseract_sinter_compat_test.py index 0ac17f7d..f85adcbf 100644 --- a/src/py/tesseract_sinter_compat_test.py +++ b/src/py/tesseract_sinter_compat_test.py @@ -525,5 +525,65 @@ def test_full_scale_one_worker(): assert result.discards == 0 assert result.shots == 1000 + +@pytest.mark.parametrize( + "det_beam, beam_climbing, no_revisit_dets, merge_errors", + [ + # Some standard values + (20, False, False, True), + # Beam climbing enabled + (20, True, False, True), + # No revisit detectors enabled + (20, False, True, True), + # Merge errors disabled + (20, False, False, False), + ] +) +def test_decode_shots_bit_packed_vs_decode_batch(det_beam, beam_climbing, no_revisit_dets, merge_errors): + """ + Compares the output of the Sinter decoder interface against the raw Tesseract decoder + to ensure they produce identical results across different configurations. + """ + + # 1. Set up the quantum circuit and detector error model. + circuit = stim.Circuit.generated( + "color_code:memory_xyz", + distance=3, + rounds=3, + after_clifford_depolarization=0.02 + ) + dem = circuit.detector_error_model() + + # 2. Create the Tesseract configuration object with the parameterized values. + config = tesseract_decoder.tesseract.TesseractConfig( + dem=dem, + ) + config.det_beam = 100 + config.beam_climbing = beam_climbing + config.no_revisit_dets = no_revisit_dets + config.merge_errors = merge_errors + + # 3. Compile the Sinter-compatible decoder. + sinter_decoder = tesseract_module.TesseractSinterDecoder(config=config) + compiled_sinter_decoder = sinter_decoder.compile_decoder_for_dem(dem=dem) + + # 4. Compile the raw Tesseract decoder directly from the config. + decoder = config.compile_decoder() + + # 5. Generate a batch of shots and unpack them for comparison. + sampler = circuit.compile_detector_sampler() + bitpacked_shots, _ = sampler.sample(shots=1000, separate_observables=True, bit_packed=True) + unpacked_shots = np.unpackbits(bitpacked_shots, bitorder='little', axis=1) + + # 6. Decode the shots using both methods. + predictions_sinter = compiled_sinter_decoder.decode_shots_bit_packed( + bit_packed_detection_event_data=bitpacked_shots) + + predictions_decode_batch = decoder.decode_batch(unpacked_shots[:, :dem.num_detectors]) + + # 7. Assert that the predictions from both decoders are identical. + assert np.array_equal(predictions_sinter, predictions_decode_batch) + + if __name__ == "__main__": raise SystemExit(pytest.main([__file__])) \ No newline at end of file From fe8a48676ce0071223dec9367d4e6b9559dc533d Mon Sep 17 00:00:00 2001 From: Dragana Grbic Date: Wed, 13 Aug 2025 15:34:03 -0700 Subject: [PATCH 12/14] Minor change --- src/py/tesseract_sinter_compat_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/tesseract_sinter_compat_test.py b/src/py/tesseract_sinter_compat_test.py index f85adcbf..b7b6f2e7 100644 --- a/src/py/tesseract_sinter_compat_test.py +++ b/src/py/tesseract_sinter_compat_test.py @@ -558,7 +558,7 @@ def test_decode_shots_bit_packed_vs_decode_batch(det_beam, beam_climbing, no_rev config = tesseract_decoder.tesseract.TesseractConfig( dem=dem, ) - config.det_beam = 100 + config.det_beam = det_beam config.beam_climbing = beam_climbing config.no_revisit_dets = no_revisit_dets config.merge_errors = merge_errors From cbf8942ea2afee9e38f68d806775e32789f1de0f Mon Sep 17 00:00:00 2001 From: Oscar Higgott Date: Thu, 14 Aug 2025 14:41:47 +0000 Subject: [PATCH 13/14] format and unpack observables in test --- src/py/tesseract_sinter_compat_test.py | 173 ++++++++++++++++--------- 1 file changed, 109 insertions(+), 64 deletions(-) diff --git a/src/py/tesseract_sinter_compat_test.py b/src/py/tesseract_sinter_compat_test.py index b7b6f2e7..7c4b5542 100644 --- a/src/py/tesseract_sinter_compat_test.py +++ b/src/py/tesseract_sinter_compat_test.py @@ -33,6 +33,7 @@ def test_tesseract_sinter_obj_exists(): assert hasattr(decoder, 'compile_decoder_for_dem') assert hasattr(decoder, 'decode_via_files') + @pytest.mark.parametrize("use_custom_config", [False, True]) def test_compile_decoder_for_dem(use_custom_config): """ @@ -55,7 +56,7 @@ def test_compile_decoder_for_dem(use_custom_config): decoder = tesseract_module.TesseractSinterDecoder(config=config) else: decoder = tesseract_module.TesseractSinterDecoder() - + compiled_decoder = decoder.compile_decoder_for_dem(dem=dem) assert compiled_decoder is not None @@ -64,10 +65,11 @@ def test_compile_decoder_for_dem(use_custom_config): # Verify the detector and observable counts are correct assert compiled_decoder.num_detectors == dem.num_detectors assert compiled_decoder.num_observables == dem.num_observables - + # Verify the config was correctly applied assert compiled_decoder.decoder.config.verbose == use_custom_config + def test_decode_shots_bit_packed(): """ Tests the 'decode_shots_bit_packed' method with a specific DEM and detection event. @@ -80,27 +82,29 @@ def test_decode_shots_bit_packed(): error(0.1) D0 D1 L0 error(0.1) D1 D2 L1 """) - + decoder = tesseract_module.TesseractSinterDecoder() compiled_decoder = decoder.compile_decoder_for_dem(dem=dem) - - num_shots = 1 - detections_array = np.zeros((num_shots, (dem.num_detectors + 7) // 8), dtype=np.uint8) - + + num_shots = 1 + detections_array = np.zeros( + (num_shots, (dem.num_detectors + 7) // 8), dtype=np.uint8) + # Set bits for detectors D0 and D1 # This should cause a logical flip on L0. - detections_array[0][0] |= (1 << 0) # D0 - detections_array[0][0] |= (1 << 1) # D1 + detections_array[0][0] |= (1 << 0) # D0 + detections_array[0][0] |= (1 << 1) # D1 + + predictions = compiled_decoder.decode_shots_bit_packed( + bit_packed_detection_event_data=detections_array) - predictions = compiled_decoder.decode_shots_bit_packed(bit_packed_detection_event_data=detections_array) - # Extract the expected predictions from the DEM - expected_predictions = np.zeros((num_shots, (dem.num_observables + 7) // 8), dtype=np.uint8) - expected_predictions[0][0] |= (1 << 0) # Logical observable L0 is flipped - + expected_predictions = np.zeros( + (num_shots, (dem.num_observables + 7) // 8), dtype=np.uint8) + expected_predictions[0][0] |= (1 << 0) # Logical observable L0 is flipped + # Compare the results assert np.array_equal(predictions, expected_predictions) - def test_decode_shots_bit_packed_multi_shot(): @@ -114,28 +118,31 @@ def test_decode_shots_bit_packed_multi_shot(): error(0.1) D0 D1 L0 error(0.1) D1 D2 L1 """) - + decoder = tesseract_module.TesseractSinterDecoder() compiled_decoder = decoder.compile_decoder_for_dem(dem=dem) - + num_shots = 3 - detections_array = np.zeros((num_shots, (dem.num_detectors + 7) // 8), dtype=np.uint8) - + detections_array = np.zeros( + (num_shots, (dem.num_detectors + 7) // 8), dtype=np.uint8) + # Shot 0: D0 and D1 fire. Expect L0 to flip. - detections_array[0][0] |= (1 << 0) # D0 - detections_array[0][0] |= (1 << 1) # D1 + detections_array[0][0] |= (1 << 0) # D0 + detections_array[0][0] |= (1 << 1) # D1 # Shot 1: D1 and D2 fire. Expect L1 to flip. - detections_array[1][0] |= (1 << 1) # D1 - detections_array[1][0] |= (1 << 2) # D2 - + detections_array[1][0] |= (1 << 1) # D1 + detections_array[1][0] |= (1 << 2) # D2 + # Shot 2: D0 and D2 fire. Expect L0 and L1 to flip. - detections_array[2][0] |= (1 << 0) # D0 - detections_array[2][0] |= (1 << 2) # D2 + detections_array[2][0] |= (1 << 0) # D0 + detections_array[2][0] |= (1 << 2) # D2 + + predictions = compiled_decoder.decode_shots_bit_packed( + bit_packed_detection_event_data=detections_array) - predictions = compiled_decoder.decode_shots_bit_packed(bit_packed_detection_event_data=detections_array) - - expected_predictions = np.zeros((num_shots, (dem.num_observables + 7) // 8), dtype=np.uint8) + expected_predictions = np.zeros( + (num_shots, (dem.num_observables + 7) // 8), dtype=np.uint8) # Expected flip for shot 0 is L0 expected_predictions[0][0] |= (1 << 0) # Expected flip for shot 1 is L1 @@ -143,7 +150,7 @@ def test_decode_shots_bit_packed_multi_shot(): # Expected flip for shot 2 is L0 and L1 expected_predictions[2][0] |= (1 << 0) expected_predictions[2][0] |= (1 << 1) - + assert np.array_equal(predictions, expected_predictions) @@ -152,7 +159,7 @@ def test_decode_via_files_sanity_check(): Tests the 'decode_via_files' method by simulating a small circuit and checking for output files. """ - + # Create a temporary directory for test files temp_dir = pathlib.Path("./temp_test_files") if temp_dir.exists(): @@ -164,7 +171,8 @@ def test_decode_via_files_sanity_check(): obs_out_path = temp_dir / "test.out.b8" # Create a small circuit and DEM file - circuit = stim.Circuit.generated("repetition_code:memory", distance=3, rounds=2) + circuit = stim.Circuit.generated( + "repetition_code:memory", distance=3, rounds=2) dem = circuit.detector_error_model() with open(dem_path, 'w') as f: f.write(str(dem)) @@ -189,12 +197,13 @@ def test_decode_via_files_sanity_check(): if temp_dir.exists(): shutil.rmtree(temp_dir) + @pytest.mark.parametrize("use_custom_config", [False, True]) def test_decode_via_files(use_custom_config): """ Tests the 'decode_via_files' method with a specific DEM and detection event. """ - + # Create a temporary directory for test files temp_dir = pathlib.Path("./temp_test_files") if temp_dir.exists(): @@ -204,7 +213,7 @@ def test_decode_via_files(use_custom_config): dem_path = temp_dir / "test.dem" dets_in_path = temp_dir / "test.b8" obs_out_path = temp_dir / "test.out.b8" - + # Create a specific DEM dem_string = """ detector(0, 0, 0) D0 @@ -216,19 +225,20 @@ def test_decode_via_files(use_custom_config): error(0.1) D2 D3 L0 """ dem = stim.DetectorErrorModel(dem_string) - + # Write the DEM string to a file with open(dem_path, 'w') as f: f.write(dem_string) detections = [0, 1] expected_predictions = np.zeros(dem.num_observables, dtype=np.uint8) - expected_predictions[0] = 1 # Flip on L0 - + expected_predictions[0] = 1 # Flip on L0 + # Pack the detection events into a bit-packed NumPy array num_shots = 1 num_detectors = dem.num_detectors - detection_events_np = np.zeros(num_shots * ((num_detectors + 7) // 8), dtype=np.uint8) + detection_events_np = np.zeros( + num_shots * ((num_detectors + 7) // 8), dtype=np.uint8) for d_idx in detections: detection_events_np[d_idx // 8] ^= (1 << (d_idx % 8)) @@ -252,11 +262,11 @@ def test_decode_via_files(use_custom_config): obs_predictions_b8_out_path=str(obs_out_path), tmp_dir=str(temp_dir) ) - + # Read the output file and unpack the results with open(obs_out_path, 'rb') as f: predictions_bytes = f.read() - + # Convert bytes to a numpy array for easy comparison predictions_np = np.frombuffer(predictions_bytes, dtype=np.uint8) unpacked_predictions = np.zeros(dem.num_observables, dtype=np.uint8) @@ -265,14 +275,14 @@ def test_decode_via_files(use_custom_config): unpacked_predictions[i] = 1 assert np.array_equal(unpacked_predictions, expected_predictions) - + # Clean up temporary files if temp_dir.exists(): shutil.rmtree(temp_dir) assert decoder.config.verbose == use_custom_config - + def test_decode_via_files_multi_shot(): """ Tests the 'decode_via_files' method with multiple shots and a specific DEM. @@ -286,7 +296,7 @@ def test_decode_via_files_multi_shot(): dem_path = temp_dir / "test.dem" dets_in_path = temp_dir / "test.b8" obs_out_path = temp_dir / "test.out.b8" - + # Create a specific DEM dem_string = """ detector(0, 0, 0) D0 @@ -296,14 +306,15 @@ def test_decode_via_files_multi_shot(): error(0.1) D1 D2 L1 """ dem = stim.DetectorErrorModel(dem_string) - + # Write the DEM string to a file with open(dem_path, 'w') as f: f.write(dem_string) num_shots = 3 num_detectors = dem.num_detectors - detection_events_np = np.zeros(num_shots * ((num_detectors + 7) // 8), dtype=np.uint8) + detection_events_np = np.zeros( + num_shots * ((num_detectors + 7) // 8), dtype=np.uint8) # Shot 0: D0 and D1 fire. Expected L0 flip. detection_events_np[0] |= (1 << 0) @@ -330,14 +341,15 @@ def test_decode_via_files_multi_shot(): obs_predictions_b8_out_path=str(obs_out_path), tmp_dir=str(temp_dir) ) - + # Read the output file and unpack the results with open(obs_out_path, 'rb') as f: predictions_bytes = f.read() predictions_np = np.frombuffer(predictions_bytes, dtype=np.uint8) - - expected_predictions_np = np.zeros(num_shots * ((dem.num_observables + 7) // 8), dtype=np.uint8) + + expected_predictions_np = np.zeros( + num_shots * ((dem.num_observables + 7) // 8), dtype=np.uint8) expected_predictions_np[0] |= (1 << 0) expected_predictions_np[1] |= (1 << 1) expected_predictions_np[2] |= (1 << 0) @@ -350,7 +362,6 @@ def test_decode_via_files_multi_shot(): shutil.rmtree(temp_dir) - def test_sinter_decode_repetition_code(): """ Tests the 'tesseract' decoder on a repetition code circuit. @@ -359,7 +370,7 @@ def test_sinter_decode_repetition_code(): rounds=3, distance=3, after_clifford_depolarization=0.05) - + result = sample_decode( circuit_obj=circuit, circuit_path=None, @@ -397,6 +408,7 @@ def test_sinter_decode_surface_code(): assert 0 <= result.errors <= 50 assert result.shots == 1000 + def test_sinter_empty(): """ Tests the 'tesseract' decoder on an empty circuit. @@ -415,6 +427,7 @@ def test_sinter_empty(): assert result.shots == 1000 assert result.errors == 0 + def test_sinter_no_observables(): """ Tests the decoder on a circuit with detectors but no logical observables. @@ -437,6 +450,7 @@ def test_sinter_no_observables(): assert result.shots == 1000 assert result.errors == 0 + def test_sinter_invincible_observables(): """ Tests the decoder on a circuit where an observable is not affected by errors. @@ -461,7 +475,6 @@ def test_sinter_invincible_observables(): assert result.errors == 0 - def test_sinter_detector_counting(): """ Tests 'that the decoder's detector count is correctly reported via Sinter'. @@ -488,8 +501,10 @@ def test_sinter_detector_counting(): ) assert result.discards == 0 assert result.custom_counts['detectors_checked'] == 20000 - assert 0.3 * 10000 * 0.5 <= result.custom_counts['detection_events'] <= 0.3 * 10000 * 2.0 - assert set(result.custom_counts.keys()) == {'detectors_checked', 'detection_events'} + assert 0.3 * 10000 * \ + 0.5 <= result.custom_counts['detection_events'] <= 0.3 * 10000 * 2.0 + assert set(result.custom_counts.keys()) == { + 'detectors_checked', 'detection_events'} def test_full_scale(): @@ -504,6 +519,7 @@ def test_full_scale(): assert result.shots == 1000 assert result.errors == 0 + def test_full_scale_one_worker(): # Create a repetition code circuit to test the decoder. circuit = stim.Circuit.generated( @@ -526,6 +542,27 @@ def test_full_scale_one_worker(): assert result.shots == 1000 +def relabel_logical_observables( + circuit: stim.Circuit, + relabel_dict: dict[int, int] +) -> stim.Circuit: + new_circuit = stim.Circuit() + for inst in circuit: + if inst.name == "OBSERVABLE_INCLUDE": + args = inst.gate_args_copy() + new_args = [relabel_dict[args[0]]] + new_inst = stim.CircuitInstruction( + name=inst.name, + targets=inst.targets_copy( + ), + gate_args=new_args, + tag=inst.tag + ) + inst = new_inst + new_circuit.append(inst) + return new_circuit + + @pytest.mark.parametrize( "det_beam, beam_climbing, no_revisit_dets, merge_errors", [ @@ -546,14 +583,19 @@ def test_decode_shots_bit_packed_vs_decode_batch(det_beam, beam_climbing, no_rev """ # 1. Set up the quantum circuit and detector error model. + p = 0.02 circuit = stim.Circuit.generated( "color_code:memory_xyz", distance=3, rounds=3, - after_clifford_depolarization=0.02 + after_clifford_depolarization=p, + before_measure_flip_probability=p, + before_round_data_depolarization=p, + after_reset_flip_probability=p ) + circuit = relabel_logical_observables(circuit=circuit, relabel_dict={0: 3}) dem = circuit.detector_error_model() - + # 2. Create the Tesseract configuration object with the parameterized values. config = tesseract_decoder.tesseract.TesseractConfig( dem=dem, @@ -566,24 +608,27 @@ def test_decode_shots_bit_packed_vs_decode_batch(det_beam, beam_climbing, no_rev # 3. Compile the Sinter-compatible decoder. sinter_decoder = tesseract_module.TesseractSinterDecoder(config=config) compiled_sinter_decoder = sinter_decoder.compile_decoder_for_dem(dem=dem) - + # 4. Compile the raw Tesseract decoder directly from the config. decoder = config.compile_decoder() - + # 5. Generate a batch of shots and unpack them for comparison. sampler = circuit.compile_detector_sampler() - bitpacked_shots, _ = sampler.sample(shots=1000, separate_observables=True, bit_packed=True) + bitpacked_shots, _ = sampler.sample( + shots=1000, separate_observables=True, bit_packed=True) unpacked_shots = np.unpackbits(bitpacked_shots, bitorder='little', axis=1) - + # 6. Decode the shots using both methods. - predictions_sinter = compiled_sinter_decoder.decode_shots_bit_packed( + predictions_sinter_bitpacked = compiled_sinter_decoder.decode_shots_bit_packed( bit_packed_detection_event_data=bitpacked_shots) - - predictions_decode_batch = decoder.decode_batch(unpacked_shots[:, :dem.num_detectors]) - + predictions_sinter = np.unpackbits( + predictions_sinter_bitpacked, bitorder='little', axis=1)[:, :dem.num_observables] + + predictions_decode_batch = decoder.decode_batch( + unpacked_shots[:, :dem.num_detectors]) # 7. Assert that the predictions from both decoders are identical. assert np.array_equal(predictions_sinter, predictions_decode_batch) if __name__ == "__main__": - raise SystemExit(pytest.main([__file__])) \ No newline at end of file + raise SystemExit(pytest.main([__file__])) From b078bb454c77bcc37b60878c2bf0dffb160ce269 Mon Sep 17 00:00:00 2001 From: Dragana Grbic Date: Thu, 14 Aug 2025 16:10:07 -0700 Subject: [PATCH 14/14] Make DEM optional argument when contructing config and compiling detector --- src/py/tesseract_test.py | 131 +++++++++++++++++++++++++++++++++++++++ src/tesseract.pybind.h | 71 ++++++++++++++++++++- 2 files changed, 201 insertions(+), 1 deletion(-) diff --git a/src/py/tesseract_test.py b/src/py/tesseract_test.py index 5df3e329..ac40f11a 100644 --- a/src/py/tesseract_test.py +++ b/src/py/tesseract_test.py @@ -56,6 +56,137 @@ def test_create_tesseract_config(): == _DETECTOR_ERROR_MODEL ) +def test_create_tesseract_config_with_dem(): + """ + Tests the constructor that takes a `dem` argument. + """ + + config = tesseract_decoder.tesseract.TesseractConfig(_DETECTOR_ERROR_MODEL) + + # Assert the string representation matches the expected format. + assert ( + str(config) + == "TesseractConfig(dem=DetectorErrorModel_Object, det_beam=65535, no_revisit_dets=0, at_most_two_errors_per_detector=0, verbose=0, merge_errors=1, pqlimit=18446744073709551615, det_orders=[], det_penalty=0, create_visualization=0)" + ) + + # Assert that the `dem` attribute is correctly set. + assert config.dem == _DETECTOR_ERROR_MODEL + +def test_create_tesseract_config_with_dem_and_custom_args(): + """ + Tests the constructor with a `dem` object and custom arguments. + """ + # Create an instance with a dem and custom arguments. + config = tesseract_decoder.tesseract.TesseractConfig( + dem=_DETECTOR_ERROR_MODEL, + det_beam=100, + merge_errors=False, + det_penalty=0.5 + ) + + # Assert that the `dem` and custom arguments are correctly set. + assert config.dem == _DETECTOR_ERROR_MODEL + assert config.det_beam == 100 + assert config.merge_errors is False + assert config.det_penalty == 0.5 + + # Assert the string representation is as expected. + assert ( + str(config) + == "TesseractConfig(dem=DetectorErrorModel_Object, det_beam=100, no_revisit_dets=0, at_most_two_errors_per_detector=0, verbose=0, merge_errors=0, pqlimit=18446744073709551615, det_orders=[], det_penalty=0.5, create_visualization=0)" + ) + +def test_compile_decoder_for_dem_basic_functionality(): + """ + Verifies that `compile_decoder_for_dem` returns a `TesseractDecoder` instance. + """ + config = tesseract_decoder.tesseract.TesseractConfig() + custom_dem = stim.DetectorErrorModel() + decoder = config.compile_decoder_for_dem(custom_dem) + + assert isinstance(decoder, tesseract_decoder.tesseract.TesseractDecoder) + +def test_compile_decoder_for_dem_sets_dem_on_config(): + """ + Ensures that the `dem` property of the TesseractConfig object is updated + before the decoder is compiled. + """ + config = tesseract_decoder.tesseract.TesseractConfig() + custom_dem = stim.DetectorErrorModel() + decoder = config.compile_decoder_for_dem(custom_dem) + + # Check that the config object itself has been updated. + assert config.dem == custom_dem + # Check that the decoder's config also reflects the change. + assert decoder.config.dem == custom_dem + +def test_compile_decoder_for_dem_preserves_other_config_params(): + """ + Tests that other custom parameters are not overwritten when the `dem` is updated. + """ + # Create a config with custom parameters. + config = tesseract_decoder.tesseract.TesseractConfig(det_beam=100, verbose=True, merge_errors=False) + + # Define a new DEM to pass to the method. + new_dem = stim.DetectorErrorModel() + decoder = config.compile_decoder_for_dem(new_dem) + + # Assert that the new decoder's config has the new dem, but retains all the other custom parameters. + assert decoder.config.dem == new_dem + assert decoder.config.det_beam == 100 + assert decoder.config.verbose is True + assert decoder.config.merge_errors is False + + +def test_compile_decoder_for_dem_with_empty_dem(): + """ + Ensures the method works correctly with an empty `dem` object. + """ + config = tesseract_decoder.tesseract.TesseractConfig(verbose=True) + + empty_dem = stim.DetectorErrorModel() + decoder = config.compile_decoder_for_dem(empty_dem) + + assert decoder.config.dem == empty_dem + assert decoder.config.verbose is True + +def test_create_tesseract_config_no_dem(): + """ + Tests the new constructor that does not require a `dem` argument. + """ + # Create an instance with no arguments. + config_default = tesseract_decoder.tesseract.TesseractConfig() + + # Assert that the `dem` attribute defaults to an empty DetectorErrorModel. + empty_dem = stim.DetectorErrorModel() + assert config_default.dem == empty_dem + + # Assert that the string representation shows the default values. + assert ( + str(config_default) + == "TesseractConfig(dem=DetectorErrorModel_Object, det_beam=65535, no_revisit_dets=0, at_most_two_errors_per_detector=0, verbose=0, merge_errors=1, pqlimit=18446744073709551615, det_orders=[], det_penalty=0, create_visualization=0)" + ) + +def test_create_tesseract_config_no_dem_with_custom_args(): + """ + Tests the new constructor with custom arguments to ensure they are passed correctly. + """ + # Create an instance with no dem but a custom det_beam. + config_custom = tesseract_decoder.tesseract.TesseractConfig(det_beam=15, verbose=True) + + # Assert that the `det_beam` and `verbose` attributes are correctly set. + assert config_custom.det_beam == 15 + assert config_custom.verbose is True + + # Assert that the `dem` attribute still defaults to an empty DetectorErrorModel. + assert config_custom.dem == stim.DetectorErrorModel() + + # Assert that the string representation reflects the custom values. + assert ( + str(config_custom) + == "TesseractConfig(dem=DetectorErrorModel_Object, det_beam=15, no_revisit_dets=0, at_most_two_errors_per_detector=0, verbose=1, merge_errors=1, pqlimit=18446744073709551615, det_orders=[], det_penalty=0, create_visualization=0)" + ) + def test_create_tesseract_decoder(): config = tesseract_decoder.tesseract.TesseractConfig(_DETECTOR_ERROR_MODEL) diff --git a/src/tesseract.pybind.h b/src/tesseract.pybind.h index fb9400d5..0724b605 100644 --- a/src/tesseract.pybind.h +++ b/src/tesseract.pybind.h @@ -32,6 +32,18 @@ std::unique_ptr _compile_tesseract_decoder_helper(const Tesser return std::make_unique(self); } +TesseractConfig tesseract_config_maker_no_dem( + int det_beam = INF_DET_BEAM, bool beam_climbing = false, bool no_revisit_dets = false, + bool at_most_two_errors_per_detector = false, bool verbose = false, bool merge_errors = true, + size_t pqlimit = std::numeric_limits::max(), + std::vector> det_orders = std::vector>(), + double det_penalty = 0.0, bool create_visualization = false) { + stim::DetectorErrorModel empty_dem; + return TesseractConfig({empty_dem, det_beam, beam_climbing, no_revisit_dets, + at_most_two_errors_per_detector, verbose, merge_errors, pqlimit, + det_orders, det_penalty, create_visualization}); +} + TesseractConfig tesseract_config_maker( py::object dem, int det_beam = INF_DET_BEAM, bool beam_climbing = false, bool no_revisit_dets = false, bool at_most_two_errors_per_detector = false, @@ -61,6 +73,41 @@ void add_tesseract_module(py::module& root) { Default constructor for TesseractConfig. Creates a new instance with default parameter values. )pbdoc") + .def(py::init(&tesseract_config_maker_no_dem), py::arg("det_beam") = INF_DET_BEAM, + py::arg("beam_climbing") = false, py::arg("no_revisit_dets") = false, + py::arg("at_most_two_errors_per_detector") = false, py::arg("verbose") = false, + py::arg("merge_errors") = true, py::arg("pqlimit") = std::numeric_limits::max(), + py::arg("det_orders") = std::vector>(), py::arg("det_penalty") = 0.0, + py::arg("create_visualization") = false, + R"pbdoc( + The constructor for the `TesseractConfig` class without a `dem` argument. + This creates an empty `DetectorErrorModel` by default. + + Parameters + ---------- + det_beam : int, default=INF_DET_BEAM + Beam cutoff that specifies the maximum number of detection events a search state can have. + beam_climbing : bool, default=False + If True, enables a beam climbing heuristic. + no_revisit_dets : bool, default=False + If True, prevents the decoder from revisiting a syndrome pattern more than once. + at_most_two_errors_per_detector : bool, default=False + If True, an optimization is enabled that assumes at most two errors + are correlated with each detector. + verbose : bool, default=False + If True, enables verbose logging from the decoder. + merge_errors : bool, default=True + If True, merges error channels that have identical syndrome patterns. + pqlimit : int, default=max_size_t + The maximum size of the priority queue. + det_orders : list[list[int]], default=empty + A list of detector orderings to use for decoding. If empty, the decoder + will generate its own orderings. + det_penalty : float, default=0.0 + A penalty value added to the cost of each detector visited. + create_visualization: bool, defualt=False + Whether to record the information needed to create a visualization or not. + )pbdoc") .def(py::init(&tesseract_config_maker), py::arg("dem"), py::arg("det_beam") = INF_DET_BEAM, py::arg("beam_climbing") = false, py::arg("no_revisit_dets") = false, py::arg("at_most_two_errors_per_detector") = false, py::arg("verbose") = false, @@ -131,7 +178,29 @@ void add_tesseract_module(py::module& root) { TesseractDecoder A new `TesseractDecoder` instance configured with the current settings. - )pbdoc"); + )pbdoc") + .def( + "compile_decoder_for_dem", + [](TesseractConfig& self, py::object dem) { + self.dem = parse_py_object(dem); + return std::make_unique(self); + }, + py::arg("dem"), py::return_value_policy::take_ownership, R"pbdoc( + Compiles the configuration into a new `TesseractDecoder` instance + for a given `dem` object. + + Parameters + ---------- + dem : stim.DetectorErrorModel + The detector error model to use for the decoder. + + Returns + ------- + TesseractDecoder + A new `TesseractDecoder` instance configured with the + provided `dem` and the other settings from this + `TesseractConfig` object. + )pbdoc"); py::class_(m, "Node", R"pbdoc( A class representing a node in the Tesseract search graph.