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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ FetchContent_MakeAvailable(stim)
# HiGHS
FetchContent_Declare(
highs
URL https://github.com/ERGO-Code/HiGHS/archive/refs/tags/v1.9.0.tar.gz
URL_HASH SHA256=dff575df08d88583c109702c7c5c75ff6e51611e6eacca8b5b3fdfba8ecc2cb4
URL https://github.com/ERGO-Code/HiGHS/archive/refs/tags/v1.14.0.tar.gz
URL_HASH SHA256=05931e8dd8c8cac514da8297003c31a206a0004d542b7da500810b85c87c20b9
)
FetchContent_MakeAvailable(highs)

Expand Down
91 changes: 91 additions & 0 deletions src/py/tesseract_sinter_compat_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,5 +687,96 @@ def test_sinter_collect_different_dems():
assert results.json_metadata["d"] == expected_distances[i]


def test_tesseract_sinter_decoder_sparsify_attributes():
decoder = TesseractSinterDecoder(
sparsify_errors=True,
sparsify_base_degree=2,
sparsify_max_degree=4,
sparsify_reactivate_limit=10,
)
assert decoder.sparsify_errors is True
assert decoder.sparsify_base_degree == 2
assert decoder.sparsify_max_degree == 4
assert decoder.sparsify_reactivate_limit == 10

# Test equality
decoder2 = TesseractSinterDecoder(
sparsify_errors=True,
sparsify_base_degree=2,
sparsify_max_degree=4,
sparsify_reactivate_limit=10,
)
assert decoder == decoder2

decoder3 = TesseractSinterDecoder(
sparsify_errors=True,
sparsify_base_degree=3, # different
sparsify_max_degree=4,
sparsify_reactivate_limit=10,
)
assert decoder != decoder3

# Test pickle
import pickle

dumped = pickle.dumps(decoder)
loaded = pickle.loads(dumped)
assert decoder == loaded


def test_make_tesseract_sinter_decoders_dict_contains_sparsify():
decoders = make_tesseract_sinter_decoders_dict()
assert "tesseract-long-beam-sparsify3" in decoders
assert "tesseract-long-beam-sparsify2" in decoders
assert "tesseract-short-beam-sparsify3" in decoders
assert "tesseract-short-beam-sparsify2" in decoders

d_long3 = decoders["tesseract-long-beam-sparsify3"]
assert d_long3.sparsify_errors is True
assert d_long3.sparsify_base_degree == 3
assert d_long3.sparsify_max_degree == -1
assert d_long3.sparsify_reactivate_limit == -1
assert d_long3.det_beam == 20

d_short2 = decoders["tesseract-short-beam-sparsify2"]
assert d_short2.sparsify_errors is True
assert d_short2.sparsify_base_degree == 2
assert d_short2.sparsify_max_degree == -1
assert d_short2.sparsify_reactivate_limit == -1
assert d_short2.det_beam == 15


@pytest.mark.parametrize(
"decoder_name",
[
"tesseract-long-beam-sparsify3",
"tesseract-long-beam-sparsify2",
"tesseract-short-beam-sparsify3",
"tesseract-short-beam-sparsify2",
],
)
def test_sinter_decode_with_sparsify_decoders(decoder_name):
# Test that the new decoders can actually run and decode a simple repetition code.
circuit = stim.Circuit.generated(
"repetition_code:memory",
rounds=3,
distance=3,
after_clifford_depolarization=0.01,
)

result = sample_decode(
circuit_obj=circuit,
circuit_path=None,
dem_obj=circuit.detector_error_model(decompose_errors=True),
dem_path=None,
num_shots=100,
decoder=decoder_name,
custom_decoders=make_tesseract_sinter_decoders_dict(),
)
assert result.discards == 0
assert result.shots == 100
assert 0 <= result.errors <= 10


if __name__ == "__main__":
raise SystemExit(pytest.main([__file__]))
80 changes: 80 additions & 0 deletions src/py/tesseract_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,5 +286,85 @@ def test_test_simplex_decode_batch_with_mismatched_syndrome_size():
)


def test_create_tesseract_config_sparsify_defaults():
config = tesseract_decoder.tesseract.TesseractConfig()
assert config.sparsify_errors is False
assert config.sparsify_base_degree == -1
assert config.sparsify_max_degree == -1
assert config.sparsify_reactivate_limit == -1


def test_create_tesseract_config_sparsify_custom():
config = tesseract_decoder.tesseract.TesseractConfig(
sparsify_errors=True,
sparsify_base_degree=2,
sparsify_max_degree=4,
sparsify_reactivate_limit=10,
)
assert config.sparsify_errors is True
assert config.sparsify_base_degree == 2
assert config.sparsify_max_degree == 4
assert config.sparsify_reactivate_limit == 10


def test_get_sparsify_reactivate_limit_heuristic():
# We need a DEM to test this because it depends on number of detectors.
# _DETECTOR_ERROR_MODEL has 2 detectors (D0, D1)
config = tesseract_decoder.tesseract.TesseractConfig(
_DETECTOR_ERROR_MODEL,
sparsify_errors=True,
sparsify_base_degree=2, # k=2
sparsify_reactivate_limit=-1, # use heuristic
)
# Heuristic formula: round((4.5^(k-2) / 3) * num_detectors)
# For k=2, num_detectors=2:
# round((4.5^0 / 3) * 2) = round((1/3) * 2) = round(2/3) = round(0.666...) = 1
assert config.get_sparsify_reactivate_limit() == 1

# Test with k=3
config.sparsify_base_degree = 3
# round((4.5^1 / 3) * 2) = round((4.5/3) * 2) = round(1.5 * 2) = round(3.0) = 3
assert config.get_sparsify_reactivate_limit() == 3

# Test when sparsify_errors is False
config.sparsify_errors = False
assert config.get_sparsify_reactivate_limit() == -1

# Test when explicitly set
config.sparsify_errors = True
config.sparsify_reactivate_limit = 5
assert config.get_sparsify_reactivate_limit() == 5

# Test validation: sparsify_base_degree < 0 throws
config.sparsify_reactivate_limit = -1
config.sparsify_base_degree = -1
with pytest.raises(ValueError, match="sparsify_base_degree must be >= 0"):
config.get_sparsify_reactivate_limit()


def test_get_sparsify_reactivate_limit_empty_dem():
config = tesseract_decoder.tesseract.TesseractConfig(
sparsify_errors=True,
sparsify_base_degree=2,
sparsify_reactivate_limit=-1,
)
assert config.get_sparsify_reactivate_limit() == 0


def test_decoder_compilation_validation():
# sparsify_base_degree < 0 throws
config = tesseract_decoder.tesseract.TesseractConfig(
_DETECTOR_ERROR_MODEL, sparsify_errors=True, sparsify_base_degree=-1
)
with pytest.raises(ValueError, match="sparsify_base_degree must be >= 0"):
config.compile_decoder()

# sparsify_max_degree < sparsify_base_degree throws
config.sparsify_base_degree = 3
config.sparsify_max_degree = 2
with pytest.raises(ValueError, match="sparsify_max_degree must be >= sparsify_base_degree"):
config.compile_decoder()


if __name__ == "__main__":
raise SystemExit(pytest.main([__file__]))
28 changes: 28 additions & 0 deletions src/tesseract.cc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include <algorithm>
#include <boost/functional/hash.hpp> // For boost::hash_range
#include <cassert>
#include <cmath>
#include <cstdint>
#include <functional> // For std::hash (though not strictly necessary here, but good practice)
#include <iostream>
Expand Down Expand Up @@ -71,6 +72,22 @@ std::string TesseractConfig::str() {
return ss.str();
}

int TesseractConfig::get_sparsify_reactivate_limit() const {
if (sparsify_reactivate_limit >= 0) {
return sparsify_reactivate_limit;
}
if (!sparsify_errors) {
return -1;
}
if (sparsify_base_degree < 0) {
throw std::invalid_argument(
"sparsify_base_degree must be >= 0 when sparsify_errors is enabled.");
}
double k = sparsify_base_degree;
double num_detectors = dem.count_detectors();
return static_cast<int>(std::round((std::pow(4.5, k - 2.0) / 3.0) * num_detectors));
}

std::string Node::str() {
std::stringstream ss;
auto& self = *this;
Expand Down Expand Up @@ -217,6 +234,17 @@ void TesseractDecoder::initialize_structures(size_t num_detectors) {
}

if (config.sparsify_errors) {
if (config.sparsify_base_degree < 0) {
throw std::invalid_argument(
"sparsify_base_degree must be >= 0 when sparsify_errors is enabled.");
}
if (config.sparsify_max_degree >= 0 &&
config.sparsify_max_degree < config.sparsify_base_degree) {
throw std::invalid_argument("sparsify_max_degree must be >= sparsify_base_degree.");
}

config.sparsify_reactivate_limit = config.get_sparsify_reactivate_limit();

sparsify_mandatory_errors.clear();
sparsify_optional_errors.clear();
for (size_t ei = 0; ei < num_errors; ++ei) {
Expand Down
1 change: 1 addition & 0 deletions src/tesseract.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ struct TesseractConfig {
int sparsify_max_degree = -1;
int sparsify_reactivate_limit = -1;

int get_sparsify_reactivate_limit() const;
std::string str();
};

Expand Down
51 changes: 45 additions & 6 deletions src/tesseract.pybind.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,27 +38,35 @@ TesseractConfig tesseract_config_maker_no_dem(
bool verbose = false, bool merge_errors = true,
size_t pqlimit = std::numeric_limits<size_t>::max(),
std::vector<std::vector<size_t>> det_orders = std::vector<std::vector<size_t>>(),
double det_penalty = 0.0, bool create_visualization = false) {
double det_penalty = 0.0, bool create_visualization = false, bool sparsify_errors = false,
int sparsify_base_degree = -1, int sparsify_max_degree = -1,
int sparsify_reactivate_limit = -1) {
stim::DetectorErrorModel empty_dem;
if (det_orders.empty()) {
det_orders = build_det_orders(empty_dem, 20, DetOrder::DetBFS, 2384753);
}
return TesseractConfig({empty_dem, det_beam, beam_climbing, no_revisit_dets, verbose,
merge_errors, pqlimit, det_orders, det_penalty, create_visualization});
merge_errors, pqlimit, det_orders, det_penalty, create_visualization,
sparsify_errors, sparsify_base_degree, sparsify_max_degree,
sparsify_reactivate_limit});
}

TesseractConfig tesseract_config_maker(
py::object dem, int det_beam = INF_DET_BEAM, bool beam_climbing = false,
bool no_revisit_dets = false, bool verbose = false, bool merge_errors = true,
size_t pqlimit = std::numeric_limits<size_t>::max(),
std::vector<std::vector<size_t>> det_orders = std::vector<std::vector<size_t>>(),
double det_penalty = 0.0, bool create_visualization = false) {
double det_penalty = 0.0, bool create_visualization = false, bool sparsify_errors = false,
int sparsify_base_degree = -1, int sparsify_max_degree = -1,
int sparsify_reactivate_limit = -1) {
stim::DetectorErrorModel input_dem = parse_py_object<stim::DetectorErrorModel>(dem);
if (det_orders.empty()) {
det_orders = build_det_orders(input_dem, 20, DetOrder::DetBFS, 2384753);
}
return TesseractConfig({input_dem, det_beam, beam_climbing, no_revisit_dets, verbose,
merge_errors, pqlimit, det_orders, det_penalty, create_visualization});
merge_errors, pqlimit, det_orders, det_penalty, create_visualization,
sparsify_errors, sparsify_base_degree, sparsify_max_degree,
sparsify_reactivate_limit});
}

}; // namespace
Expand All @@ -82,7 +90,9 @@ void add_tesseract_module(py::module& root) {
py::arg("beam_climbing") = false, py::arg("no_revisit_dets") = true,
py::arg("verbose") = false, py::arg("merge_errors") = true, py::arg("pqlimit") = 200000,
py::arg("det_orders") = std::vector<std::vector<size_t>>(), py::arg("det_penalty") = 0.0,
py::arg("create_visualization") = false,
py::arg("create_visualization") = false, py::arg("sparsify_errors") = false,
py::arg("sparsify_base_degree") = -1, py::arg("sparsify_max_degree") = -1,
py::arg("sparsify_reactivate_limit") = -1,
R"pbdoc(
The constructor for the `TesseractConfig` class without a `dem` argument.
This creates an empty `DetectorErrorModel` by default.
Expand All @@ -109,12 +119,22 @@ void add_tesseract_module(py::module& root) {
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.
sparsify_errors: bool, default=False
If True, enables per-shot sparse error activation.
sparsify_base_degree: int, default=-1
Maximum detector degree for mandatory errors.
sparsify_max_degree: int, default=-1
Maximum detector degree for optional errors.
sparsify_reactivate_limit: int, default=-1
Maximum number of optional errors to reactivate per shot. Use -1 for heuristic default.
)pbdoc")
.def(py::init(&tesseract_config_maker), py::arg("dem"), py::arg("det_beam") = 5,
py::arg("beam_climbing") = false, py::arg("no_revisit_dets") = true,
py::arg("verbose") = false, py::arg("merge_errors") = true, py::arg("pqlimit") = 200000,
py::arg("det_orders") = std::vector<std::vector<size_t>>(), py::arg("det_penalty") = 0.0,
py::arg("create_visualization") = false,
py::arg("create_visualization") = false, py::arg("sparsify_errors") = false,
py::arg("sparsify_base_degree") = -1, py::arg("sparsify_max_degree") = -1,
py::arg("sparsify_reactivate_limit") = -1,
R"pbdoc(
The constructor for the `TesseractConfig` class.

Expand Down Expand Up @@ -142,6 +162,14 @@ void add_tesseract_module(py::module& root) {
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.
sparsify_errors: bool, default=False
If True, enables per-shot sparse error activation.
sparsify_base_degree: int, default=-1
Maximum detector degree for mandatory errors.
sparsify_max_degree: int, default=-1
Maximum detector degree for optional errors.
sparsify_reactivate_limit: int, default=-1
Maximum number of optional errors to reactivate per shot. Use -1 for heuristic default.
)pbdoc")
.def_property("dem", &dem_getter<TesseractConfig>, &dem_setter<TesseractConfig>,
"The `stim.DetectorErrorModel` that defines the error channels and detectors.")
Expand All @@ -164,6 +192,17 @@ void add_tesseract_module(py::module& root) {
"The penalty cost added for each detector.")
.def_readwrite("create_visualization", &TesseractConfig::create_visualization,
"If True, records necessary information to create visualization.")
.def_readwrite("sparsify_errors", &TesseractConfig::sparsify_errors,
"If True, enables per-shot sparse error activation.")
.def_readwrite("sparsify_base_degree", &TesseractConfig::sparsify_base_degree,
"Maximum detector degree for mandatory errors.")
.def_readwrite("sparsify_max_degree", &TesseractConfig::sparsify_max_degree,
"Maximum detector degree for optional errors.")
.def_readwrite("sparsify_reactivate_limit", &TesseractConfig::sparsify_reactivate_limit,
"Maximum number of optional errors to reactivate per shot. Use -1 for "
"heuristic default.")
.def("get_sparsify_reactivate_limit", &TesseractConfig::get_sparsify_reactivate_limit,
"Returns the resolved reactivate limit, applying the heuristic if it is set to -1.")
.def("__str__", &TesseractConfig::str)
.def("compile_decoder", &_compile_tesseract_decoder_helper,
py::return_value_policy::take_ownership,
Expand Down
12 changes: 3 additions & 9 deletions src/tesseract_main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -334,16 +334,10 @@ struct Args {
config.sparsify_errors = sparsify_errors;
config.sparsify_base_degree = sparsify_base_degree;
config.sparsify_max_degree = sparsify_max_degree;

// Apply heuristic estimate for number of errors if sparsify_errors is enabled but no limit was
// provided
if (sparsify_errors && sparsify_reactivate_limit < 0) {
double k = sparsify_base_degree;
double num_detectors = config.dem.count_detectors();
sparsify_reactivate_limit =
static_cast<int>(std::round((std::pow(4.5, k - 2.0) / 3.0) * num_detectors));
}
config.sparsify_reactivate_limit = sparsify_reactivate_limit;

config.sparsify_reactivate_limit = config.get_sparsify_reactivate_limit();
sparsify_reactivate_limit = config.sparsify_reactivate_limit;
}
};

Expand Down
Loading
Loading