From 6f5afc969c76efec4eb806ad3f705eaec4be9347 Mon Sep 17 00:00:00 2001 From: Noah Shutty Date: Thu, 5 Jun 2025 15:14:35 -0700 Subject: [PATCH 1/4] fix typo in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 34a773f..9192d4b 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ A Search-Based Decoder for Quantum Error Correction. Tesseract is a Most Likely Error decoder designed for Low Density Parity Check (LDPC) quantum error-correcting codes. It applies pruning heuristics and manifold orientation techniques during a search over the error subsets to identify the most likely error configuration consistent with the -observed syndrome. Tesseract archives significant speed improvements over traditional integer +observed syndrome. Tesseract achieves significant speed improvements over traditional integer programming-based decoders while maintaining comparable accuracy at moderate physical error rates. We tested the Tesseract decoder for: From 64e20ff0d63855faadd3fbf7dfb83408b26e4ded Mon Sep 17 00:00:00 2001 From: Noah Shutty Date: Sat, 7 Jun 2025 12:02:52 -0700 Subject: [PATCH 2/4] Fix Bazel build on Linux by disabling LTO --- src/BUILD | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/BUILD b/src/BUILD index d0dae30..7634a25 100644 --- a/src/BUILD +++ b/src/BUILD @@ -27,7 +27,6 @@ OPT_COPTS = select({ "//conditions:default": [ "-Ofast", "-fno-fast-math", - "-flto", "-march=native", ], ":debug": ["-Og"], @@ -42,7 +41,6 @@ OPT_LINKOPTS = select({ "//conditions:default": [ "-Ofast", "-fno-fast-math", - "-flto", "-march=native", ], ":debug": [], @@ -141,6 +139,7 @@ cc_test( cc_test( name = "common_tests", srcs = ["common.test.cc"], + linkstatic = True, deps = [ ":libcommon", "@gtest", From 9bfba58c78d214cbd2e40b9bbb4e2d71cf5c5189 Mon Sep 17 00:00:00 2001 From: Noah Shutty Date: Sat, 7 Jun 2025 15:45:48 -0700 Subject: [PATCH 3/4] Sanitize DEM usage and enforce no zero-probability errors --- README.md | 3 +- src/BUILD | 2 ++ src/common.cc | 47 ++++++++++++++++++++----- src/common.h | 7 ++++ src/common.test.cc | 80 +++++++++++++++++++++++++++++++++++++++++++ src/simplex.cc | 7 ++-- src/simplex_main.cc | 1 + src/tesseract.cc | 1 + src/tesseract.perf.cc | 2 ++ src/tesseract.test.cc | 21 ++++++++++++ src/tesseract_main.cc | 1 + 11 files changed, 159 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 34a773f..b988e30 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,8 @@ We tested the Tesseract decoder for: * **Stim and DEM Support:** processes [Stim](https://github.com/quantumlib/stim) circuit files and [Detector Error Model (DEM)](https://github.com/quantumlib/Stim/blob/main/doc/file_format_dem_detector_error_model.md) - files with arbitrary error models. + files with arbitrary error models. Zero-probability error instructions are + automatically removed when a DEM is loaded. * **Parallel Decoding:** uses multithreading to accelerate the decoding process, making it suitable for large-scale simulations. * **Efficient Beam Search:** implements a [beam search](https://en.wikipedia.org/wiki/Beam_search) diff --git a/src/BUILD b/src/BUILD index d0dae30..6e600b1 100644 --- a/src/BUILD +++ b/src/BUILD @@ -141,6 +141,8 @@ cc_test( cc_test( name = "common_tests", srcs = ["common.test.cc"], + copts = OPT_COPTS, + linkopts = OPT_LINKOPTS, deps = [ ":libcommon", "@gtest", diff --git a/src/common.cc b/src/common.cc index 3f36fd6..fb5439d 100644 --- a/src/common.cc +++ b/src/common.cc @@ -113,6 +113,29 @@ stim::DetectorErrorModel common::merge_identical_errors( return out_dem; } +stim::DetectorErrorModel common::remove_zero_probability_errors( + const stim::DetectorErrorModel& dem) { + stim::DetectorErrorModel out_dem; + for (const stim::DemInstruction& instruction : dem.flattened().instructions) { + switch (instruction.type) { + case stim::DemInstructionType::DEM_SHIFT_DETECTORS: + assert(false && "unreachable"); + break; + case stim::DemInstructionType::DEM_ERROR: + if (instruction.arg_data[0] > 0) { + out_dem.append_dem_instruction(instruction); + } + break; + case stim::DemInstructionType::DEM_DETECTOR: + out_dem.append_dem_instruction(instruction); + break; + default: + assert(false && "unreachable"); + } + } + return out_dem; +} + stim::DetectorErrorModel common::dem_from_counts( stim::DetectorErrorModel& orig_dem, const std::vector& error_counts, size_t num_shots) { @@ -121,6 +144,17 @@ stim::DetectorErrorModel common::dem_from_counts( "Error hits array must be the same size as the number of errors in the " "original DEM."); } + + for (const stim::DemInstruction& instruction : + orig_dem.flattened().instructions) { + if (instruction.type == stim::DemInstructionType::DEM_ERROR && + instruction.arg_data[0] == 0) { + throw std::invalid_argument( + "dem_from_counts requires DEMs without zero-probability errors. Use" + " remove_zero_probability_errors first."); + } + } + stim::DetectorErrorModel out_dem; size_t ei = 0; for (const stim::DemInstruction& instruction : @@ -130,14 +164,11 @@ stim::DetectorErrorModel common::dem_from_counts( assert(false && "unreachable"); break; case stim::DemInstructionType::DEM_ERROR: { - // Ignore zero-probability errors - if (instruction.arg_data[0] > 0) { - double est_probability = - double(error_counts.at(ei)) / double(num_shots); - out_dem.append_error_instruction(est_probability, - instruction.target_data, /*tag=*/""); - ++ei; - } + double est_probability = + double(error_counts.at(ei)) / double(num_shots); + out_dem.append_error_instruction(est_probability, + instruction.target_data, /*tag=*/""); + ++ei; break; } case stim::DemInstructionType::DEM_DETECTOR: { diff --git a/src/common.h b/src/common.h index 0998be2..754ec28 100644 --- a/src/common.h +++ b/src/common.h @@ -71,8 +71,15 @@ struct Error { stim::DetectorErrorModel merge_identical_errors( const stim::DetectorErrorModel& dem); +// Returns a copy of the given error model with any zero-probability DEM_ERROR +// instructions removed. +stim::DetectorErrorModel remove_zero_probability_errors( + const stim::DetectorErrorModel& dem); + // Makes a new dem where the probabilities of errors are estimated from the // fraction of shots they were used in. +// Throws std::invalid_argument if `orig_dem` contains zero-probability errors; +// call remove_zero_probability_errors first. stim::DetectorErrorModel dem_from_counts( stim::DetectorErrorModel& orig_dem, const std::vector& error_counts, size_t num_shots); diff --git a/src/common.test.cc b/src/common.test.cc index ffed4e6..a3a92df 100644 --- a/src/common.test.cc +++ b/src/common.test.cc @@ -25,3 +25,83 @@ TEST(common, ErrorsStructFromDemInstruction) { EXPECT_EQ(ES.symptom.detectors, std::vector{1}); EXPECT_EQ(ES.symptom.observables, 0b01); } + +TEST(common, DemFromCountsRejectsZeroProbabilityErrors) { + stim::DetectorErrorModel dem(R"DEM( + error(0.1) D0 + error(0) D1 + error(0.2) D2 + detector(0, 0, 0) D0 + detector(0, 0, 0) D1 + detector(0, 0, 0) D2 + )DEM"); + + std::vector counts{1, 7, 4}; + size_t num_shots = 10; + EXPECT_THROW({ + common::dem_from_counts(dem, counts, num_shots); + }, std::invalid_argument); + + stim::DetectorErrorModel cleaned = common::remove_zero_probability_errors(dem); + stim::DetectorErrorModel out_dem = + common::dem_from_counts(cleaned, std::vector{1, 4}, num_shots); + + auto flat = out_dem.flattened(); + ASSERT_EQ(out_dem.count_errors(), 2); + ASSERT_GE(flat.instructions.size(), 2); + + EXPECT_EQ(flat.instructions[0].type, + stim::DemInstructionType::DEM_ERROR); + EXPECT_NEAR(flat.instructions[0].arg_data[0], 0.1, 1e-9); + ASSERT_EQ(flat.instructions[1].type, + stim::DemInstructionType::DEM_ERROR); + EXPECT_NEAR(flat.instructions[1].arg_data[0], 0.4, 1e-9); +} + +TEST(common, DemFromCountsSimpleTwoErrors) { + stim::DetectorErrorModel dem(R"DEM( + error(0.25) D0 + error(0.35) D1 + detector(0, 0, 0) D0 + detector(0, 0, 0) D1 + )DEM"); + + std::vector counts{5, 7}; + size_t num_shots = 20; + stim::DetectorErrorModel out_dem = + common::dem_from_counts(dem, counts, num_shots); + + auto flat = out_dem.flattened(); + ASSERT_EQ(out_dem.count_errors(), 2); + + ASSERT_GE(flat.instructions.size(), 2); + EXPECT_EQ(flat.instructions[0].type, + stim::DemInstructionType::DEM_ERROR); + EXPECT_NEAR(flat.instructions[0].arg_data[0], 0.25, 1e-9); + EXPECT_EQ(flat.instructions[1].type, + stim::DemInstructionType::DEM_ERROR); + EXPECT_NEAR(flat.instructions[1].arg_data[0], 0.35, 1e-9); +} + +TEST(common, RemoveZeroProbabilityErrors) { + stim::DetectorErrorModel dem(R"DEM( + error(0.1) D0 + error(0) D1 + error(0.2) D2 + detector(0, 0, 0) D0 + detector(0, 0, 0) D1 + detector(0, 0, 0) D2 + )DEM"); + + stim::DetectorErrorModel cleaned = + common::remove_zero_probability_errors(dem); + + EXPECT_EQ(cleaned.count_errors(), 2); + auto flat = cleaned.flattened(); + ASSERT_EQ(flat.instructions[0].type, + stim::DemInstructionType::DEM_ERROR); + EXPECT_NEAR(flat.instructions[0].arg_data[0], 0.1, 1e-9); + ASSERT_EQ(flat.instructions[1].type, + stim::DemInstructionType::DEM_ERROR); + EXPECT_NEAR(flat.instructions[1].arg_data[0], 0.2, 1e-9); +} diff --git a/src/simplex.cc b/src/simplex.cc index ad261b9..cbe0c94 100644 --- a/src/simplex.cc +++ b/src/simplex.cc @@ -22,6 +22,7 @@ constexpr size_t T_COORD = 2; SimplexDecoder::SimplexDecoder(SimplexConfig _config) : config(_config) { + config.dem = common::remove_zero_probability_errors(config.dem); std::vector detector_t_coords(config.dem.count_detectors()); for (const stim::DemInstruction& instruction : config.dem.flattened().instructions) { @@ -30,10 +31,8 @@ SimplexDecoder::SimplexDecoder(SimplexConfig _config) : config(_config) { assert(false && "unreachable"); break; case stim::DemInstructionType::DEM_ERROR: { - // Ignore zero-probability errors - if (instruction.arg_data[0] > 0) { - errors.emplace_back(instruction); - } + assert(instruction.arg_data[0] > 0); + errors.emplace_back(instruction); break; } case stim::DemInstructionType::DEM_DETECTOR: diff --git a/src/simplex_main.cc b/src/simplex_main.cc index 10d6e1e..e4cb9eb 100644 --- a/src/simplex_main.cc +++ b/src/simplex_main.cc @@ -175,6 +175,7 @@ struct Args { if (!no_merge_errors) { config.dem = common::merge_identical_errors(config.dem); } + config.dem = common::remove_zero_probability_errors(config.dem); if (sample_num_shots > 0) { assert(!circuit_path.empty()); diff --git a/src/tesseract.cc b/src/tesseract.cc index 2c002b4..98db9f4 100644 --- a/src/tesseract.cc +++ b/src/tesseract.cc @@ -38,6 +38,7 @@ double TesseractDecoder::get_detcost(size_t d, } TesseractDecoder::TesseractDecoder(TesseractConfig config_) : config(config_) { + config.dem = common::remove_zero_probability_errors(config.dem); if (config.det_orders.empty()) { config.det_orders.emplace_back(config.dem.count_detectors()); std::iota(config.det_orders[0].begin(), config.det_orders[0].end(), 0); diff --git a/src/tesseract.perf.cc b/src/tesseract.perf.cc index aa6147e..38e49e1 100644 --- a/src/tesseract.perf.cc +++ b/src/tesseract.perf.cc @@ -77,6 +77,7 @@ void benchmark_tesseract(std::string circuit_path, size_t num_shots) { /*approximate_disjoint_errors_threshold=*/1, /*ignore_decomposition_failures=*/false, /*block_decomposition_from_introducing_remnant_edges=*/false); + dem = common::remove_zero_probability_errors(dem); TesseractConfig config{dem}; config.det_beam = 20; config.pqlimit = 10'000'000; @@ -99,6 +100,7 @@ void benchmark_simplex(std::string circuit_path, size_t num_shots) { /*approximate_disjoint_errors_threshold=*/1, /*ignore_decomposition_failures=*/false, /*block_decomposition_from_introducing_remnant_edges=*/false); + dem = common::remove_zero_probability_errors(dem); SimplexConfig config{dem}; config.parallelize = true; SimplexDecoder decoder(config); diff --git a/src/tesseract.test.cc b/src/tesseract.test.cc index b70ac5f..d18fe54 100644 --- a/src/tesseract.test.cc +++ b/src/tesseract.test.cc @@ -195,3 +195,24 @@ TEST(tesseract, Tesseract_simplex_DEM_exhaustive_test) { ASSERT_TRUE(return_val); } } + +TEST(tesseract, DecodersStripZeroProbabilityErrors) { + stim::DetectorErrorModel dem(R"DEM( + error(0.1) D0 + error(0) D1 + error(0.2) D2 + detector(0,0,0) D0 + detector(0,0,0) D1 + detector(0,0,0) D2 + )DEM"); + + TesseractConfig t_config{dem}; + TesseractDecoder t_dec(t_config); + EXPECT_EQ(t_dec.config.dem.count_errors(), 2); + EXPECT_EQ(t_dec.errors.size(), 2); + + SimplexConfig s_config{dem}; + SimplexDecoder s_dec(s_config); + EXPECT_EQ(s_dec.config.dem.count_errors(), 2); + EXPECT_EQ(s_dec.errors.size(), 2); +} diff --git a/src/tesseract_main.cc b/src/tesseract_main.cc index eb209f4..d4ab226 100644 --- a/src/tesseract_main.cc +++ b/src/tesseract_main.cc @@ -170,6 +170,7 @@ struct Args { if (!no_merge_errors) { config.dem = common::merge_identical_errors(config.dem); } + config.dem = common::remove_zero_probability_errors(config.dem); // Sample orientations of the error model to use for the det priority { From 7174ce48de82996bb1447601c923fcf31e29ae2b Mon Sep 17 00:00:00 2001 From: Noah Shutty Date: Mon, 9 Jun 2025 18:20:17 -0700 Subject: [PATCH 4/4] Add short and long test modes --- README.md | 13 +++++++++++++ src/tesseract.test.cc | 18 ++++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 276da35..9155ecb 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,19 @@ Tesseract uses [Bazel](https://bazel.build/) as its build system. To build the d bazel build src:all ``` +## Running Tests + +Unit tests are executed with Bazel. Run the quick test suite using: +```bash +bazel test //src:all +``` +By default the tests use reduced parameters and finish in under 30 seconds. +To run a more exhaustive suite with additional shots and larger distances, set: +```bash +TESSERACT_LONG_TESTS=1 bazel test //src:all +``` + + ## Usage The file `tesseract_main.cc` provides the main entry point for Tesseract Decoder. It can decode diff --git a/src/tesseract.test.cc b/src/tesseract.test.cc index d18fe54..a1775c7 100644 --- a/src/tesseract.test.cc +++ b/src/tesseract.test.cc @@ -15,6 +15,7 @@ #include "tesseract.h" #include +#include #include "gtest/gtest.h" #include "simplex.h" @@ -79,10 +80,19 @@ bool simplex_test_compare(stim::DetectorErrorModel& dem, } TEST(tesseract, Tesseract_simplex_test) { - for (float p_err : {0.001, 0.003, 0.005}) { - for (size_t distance : {3, 5}) { - for (const size_t num_rounds : {2, 5, 10}) { - const size_t num_shots = 1000 / num_rounds / distance; + bool long_tests = std::getenv("TESSERACT_LONG_TESTS") != nullptr; + auto p_errs = long_tests ? std::vector{0.001f, 0.003f, 0.005f} + : std::vector{0.003f}; + auto distances = long_tests ? std::vector{3, 5, 7} + : std::vector{3}; + auto rounds = long_tests ? std::vector{2, 5, 10} + : std::vector{2}; + size_t base_shots = long_tests ? 1000 : 100; + + for (float p_err : p_errs) { + for (size_t distance : distances) { + for (const size_t num_rounds : rounds) { + const size_t num_shots = base_shots / num_rounds / distance; std::cout << "p_err = " << p_err << " distance = " << distance << " num_rounds = " << num_rounds << " num_shots = " << num_shots << std::endl;