Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,66 @@ print(f"Predicted errors indices: {predicted_errors}")
for i in predicted_errors:
print(f" {i}: {decoder.errors[i]}")
```
## Using Tesseract with Sinter

Tesseract can be easily integrated into [Sinter](https://github.com/quantumlib/Sinter) workflows. Sinter is a tool for running and organizing quantum error correction simulations. The `tesseract_sinter_compat` module provides the necessary interface.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Tesseract can be easily integrated into [Sinter](https://github.com/quantumlib/Sinter) workflows. Sinter is a tool for running and organizing quantum error correction simulations. The `tesseract_sinter_compat` module provides the necessary interface.
Tesseract can be easily integrated into [sinter](https://github.com/quantumlib/Stim/tree/main/glue/sample) workflows. Sinter is a tool for running and organizing quantum error correction simulations. The `tesseract_sinter_compat` module provides the necessary interface.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, thanks.


Here's an example of how to use Tesseract as a decoder for multiple Sinter tasks:

```python
import stim
import sinter
from tesseract_decoder import tesseract_sinter_compat

# Define a list of Sinter task(s) with different circuits/decoders.
tasks = []
# These are the sensible defaults given by tesseract_module.make_tesseract_sinter_decoders_dict().
decoders = ['tesseract', 'tesseract-long-beam', 'tesseract-short-beam']
for i, distance in enumerate([3, 5, 7]):
circuit = stim.Circuit.generated(
"repetition_code:memory",
distance=distance,
rounds=3,
after_clifford_depolarization=0.1
)
tasks.append(sinter.Task(
circuit=circuit,
decoder=decoders[i],
json_metadata={"d": distance, "decoder": decoders[i]},
))

Copy link
Contributor

Choose a reason for hiding this comment

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

Note that from here on needs to be after an:

if __name__ == "__main__":

Might be worth making sure that the example code works right away after copy-pasting (e.g. without the above fix the sinter error can be a bit confusing...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Made some tweaks and tested it in a standalone python file.

# Collect decoding outcomes per task from Sinter.
results = sinter.collect(
num_workers=2,
tasks=tasks,
max_shots=10000,
decoders=decoders,
custom_decoders=tesseract_module.make_tesseract_sinter_decoders_dict(),
Copy link
Contributor

@oscarhiggott oscarhiggott Oct 24, 2025

Choose a reason for hiding this comment

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

Suggested change
custom_decoders=tesseract_module.make_tesseract_sinter_decoders_dict(),
custom_decoders=make_tesseract_sinter_decoders_dict(),

)

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it might be good to include one example of using the tesseract_decoder.TesseractSinterDecoder constructor with a full set of custom arguments. E.g. here's a full example (though maybe could be made more concise if combined with the example above):

import stim
import sinter
from tesseract_decoder import TesseractSinterDecoder
import tesseract_decoder

if __name__ == "__main__":
    custom_sinter_decoder = TesseractSinterDecoder(
        det_beam=10,
        beam_climbing=True,
        no_revisit_dets=True,
        merge_errors=True,
        pqlimit=1_000,
        num_det_orders=5,
        det_order_method=tesseract_decoder.utils.DetOrder.DetIndex,
        seed=2384753,
    )

    p = 0.005
    tasks = [
        sinter.Task(
            circuit=stim.Circuit.generated(
                "surface_code:rotated_memory_x",
                distance=d,
                rounds=d,
                after_clifford_depolarization=p,
            ),
            json_metadata={"d": d, "r": d, "p": p},
        )
        for d in (3, 5)
    ]

    results = sinter.collect(
        num_workers=2,
        tasks=tasks,
        max_shots=10_000,
        decoders=["custom-tesseract-decoder"],
        custom_decoders={"custom-tesseract-decoder": custom_sinter_decoder},
        print_progress=True,
    )

for result in results:
print(f"task metadata = {result.json_metadata}")
print(f" Shots run: {result.shots}")
print(f" Observed errors: {result.errors}")
print(f" Logical error rate: {result.errors / result.shots}")

# Should get something like:
# task metadata = {'d': 5, 'decoder': 'tesseract-long-beam'}
# Shots run: 10000
# Observed errors: 315
# Logical error rate: 0.0315
# task metadata = {'d': 3, 'decoder': 'tesseract'}
# Shots run: 10000
# Observed errors: 654
# Logical error rate: 0.0654
# task metadata = {'d': 7, 'decoder': 'tesseract-short-beam'}
# Shots run: 10000
# Observed errors: 153
# Logical error rate: 0.0153
```

This example runs simulations for a repetition code with different distances [3, 5, 7] with different Tesseract default decoders. Sinter efficiently manages the execution of these tasks, and Tesseract is used for decoding. For more usage examples, see the tests in `src/py/tesseract_sinter_compat_test.py`.
Copy link
Contributor

@oscarhiggott oscarhiggott Oct 23, 2025

Choose a reason for hiding this comment

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

Perhaps we could add a command line example here too, e.g.:

This example runs simulations for a repetition code with different distances [3, 5, 7] with different Tesseract default decoders.

Sinter can also be used at the command line. Here is an example of this using Tesseract:

sinter collect \
    --circuits "example_circuit.stim" \
    --decoders tesseract-long-beam \
    --custom_decoders_module_function "tesseract_decoder:make_tesseract_sinter_decoders_dict" \
    --max_shots 100_000 \
    --max_errors 100
    --processes auto \
    --save_resume_filepath "stats.csv" \

Sinter efficiently manages the execution of these tasks, and Tesseract is used for decoding. For more usage examples, see the tests in src/py/tesseract_sinter_compat_test.py.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

SG I'll add that in.


## Good Starting Points for Tesseract Configurations:
The [Tesseract paper](https://arxiv.org/pdf/2503.10988) recommends two setup for starting your exploration with tesseract:

Expand Down
72 changes: 56 additions & 16 deletions src/py/tesseract_sinter_compat_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ def test_compile_decoder_for_dem(use_custom_config):
""")

if use_custom_config:
config = tesseract_decoder.tesseract.TesseractConfig()
config.verbose = True
decoder = tesseract_module.TesseractSinterDecoder(config=config)
decoder = tesseract_module.TesseractSinterDecoder(
verbose=True,
)
else:
decoder = tesseract_module.TesseractSinterDecoder()

Expand Down Expand Up @@ -247,9 +247,9 @@ def test_decode_via_files(use_custom_config):
f.write(detection_events_np.tobytes())

if use_custom_config:
config = tesseract_decoder.tesseract.TesseractConfig()
config.verbose = True
decoder = tesseract_module.TesseractSinterDecoder(config=config)
decoder = tesseract_module.TesseractSinterDecoder(
verbose=True,
)
else:
decoder = tesseract_module.TesseractSinterDecoder()

Expand Down Expand Up @@ -280,7 +280,7 @@ def test_decode_via_files(use_custom_config):
if temp_dir.exists():
shutil.rmtree(temp_dir)

assert decoder.config.verbose == use_custom_config
assert decoder.verbose == use_custom_config


def test_decode_via_files_multi_shot():
Expand Down Expand Up @@ -596,20 +596,25 @@ def test_decode_shots_bit_packed_vs_decode_batch(det_beam, beam_climbing, no_rev
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,
# 2. Compile the Sinter-compatible decoder with the parameterized values for the DEM.
sinter_decoder = tesseract_module.TesseractSinterDecoder(
det_beam=det_beam,
beam_climbing=beam_climbing,
no_revisit_dets=no_revisit_dets,
merge_errors=merge_errors,
)
config.det_beam = det_beam
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.
# 4. Obtain the compiled decoder from the config.
config = tesseract_decoder.tesseract.TesseractConfig(
dem=dem,
det_beam=det_beam,
beam_climbing=beam_climbing,
no_revisit_dets=no_revisit_dets,
merge_errors=merge_errors,
)
decoder = config.compile_decoder()

# 5. Generate a batch of shots and unpack them for comparison.
Expand All @@ -630,5 +635,40 @@ def test_decode_shots_bit_packed_vs_decode_batch(det_beam, beam_climbing, no_rev
assert np.array_equal(predictions_sinter, predictions_decode_batch)


def test_sinter_collect_different_dems():
"""
Ensures that Sinter tasks compile with different DEMs before collection.
"""
# Create a repetition code circuit to test the decoder.
min_distance = 3
max_distance = 7
tasks = [
sinter.Task(
circuit=stim.Circuit.generated(
"repetition_code:memory",
distance=d,
rounds=3,
after_clifford_depolarization=0.1,
),
json_metadata={"d": d},
)
for d in range(min_distance, max_distance + 1, 2)
]

# Use sinter.collect to run the decoding task.
all_results = sinter.collect(
num_workers=1,
tasks=tasks,
decoders=["tesseract-long-beam"],
max_shots=100, # Reduced max_shots for testing
custom_decoders=tesseract_module.make_tesseract_sinter_decoders_dict()
)

assert len(all_results) == len(tasks)
expected_distances = [3,5,7]
for i, results in enumerate(all_results):
assert results.json_metadata['d'] == expected_distances[i]


if __name__ == "__main__":
raise SystemExit(pytest.main([__file__]))
139 changes: 102 additions & 37 deletions src/tesseract_sinter_compat.pybind.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,17 +100,59 @@ struct TesseractSinterCompiledDecoder {
// a decoder for a specific Detector Error Model (DEM).
//--------------------------------------------------------------------------------------------------
struct TesseractSinterDecoder {
// Use TesseractConfig as an integrated property.
TesseractConfig config;
// Parameters for TesseractConfig
int det_beam;
bool beam_climbing;
bool no_revisit_dets;
bool verbose;
bool merge_errors;
size_t pqlimit;
double det_penalty;
bool create_visualization;

// Parameters for build_det_orders
size_t num_det_orders;
DetOrder det_order_method;
uint64_t seed;

// Default constructor
TesseractSinterDecoder() : config(TesseractConfig()) {}

// Constructor with TesseractConfig parameter
TesseractSinterDecoder(const TesseractConfig& config_in) : config(config_in) {}
TesseractSinterDecoder()
: det_beam(DEFAULT_DET_BEAM),
beam_climbing(false),
no_revisit_dets(true),
verbose(false),
merge_errors(true),
pqlimit(DEFAULT_PQLIMIT),
det_penalty(0.0),
create_visualization(false),
num_det_orders(0),
det_order_method(DetOrder::DetBFS),
seed(2384753) {}

// Constructor with parameters
TesseractSinterDecoder(int det_beam, bool beam_climbing, bool no_revisit_dets, bool verbose,
bool merge_errors, size_t pqlimit, double det_penalty,
bool create_visualization, size_t num_det_orders,
DetOrder det_order_method, uint64_t seed)
: det_beam(det_beam),
beam_climbing(beam_climbing),
no_revisit_dets(no_revisit_dets),
verbose(verbose),
merge_errors(merge_errors),
pqlimit(pqlimit),
det_penalty(det_penalty),
create_visualization(create_visualization),
num_det_orders(num_det_orders),
det_order_method(det_order_method),
seed(seed) {}

bool operator==(const TesseractSinterDecoder& other) const {
return true;
return det_beam == other.det_beam && beam_climbing == other.beam_climbing &&
no_revisit_dets == other.no_revisit_dets && verbose == other.verbose &&
merge_errors == other.merge_errors && pqlimit == other.pqlimit &&
det_penalty == other.det_penalty && create_visualization == other.create_visualization &&
num_det_orders == other.num_det_orders && det_order_method == other.det_order_method &&
seed == other.seed;
}

bool operator!=(const TesseractSinterDecoder& other) const {
Expand All @@ -121,8 +163,12 @@ struct TesseractSinterDecoder {
TesseractSinterCompiledDecoder compile_decoder_for_dem(const py::object& dem) {
const stim::DetectorErrorModel stim_dem(py::cast<std::string>(py::str(dem)).c_str());

TesseractConfig local_config = config;
local_config.dem = stim_dem;
std::vector<std::vector<size_t>> det_orders =
build_det_orders(stim_dem, num_det_orders, det_order_method, seed);

TesseractConfig local_config = {
stim_dem, det_beam, beam_climbing, no_revisit_dets, verbose,
merge_errors, pqlimit, det_orders, det_penalty, create_visualization};
auto decoder = std::make_unique<TesseractDecoder>(local_config);

return TesseractSinterCompiledDecoder{
Expand Down Expand Up @@ -151,9 +197,13 @@ struct TesseractSinterDecoder {
dem_file.close();

// Construct TesseractDecoder.
TesseractConfig local_config = config;
const stim::DetectorErrorModel stim_dem(dem_content_str.c_str());
local_config.dem = stim_dem;
std::vector<std::vector<size_t>> det_orders =
build_det_orders(stim_dem, num_det_orders, det_order_method, seed);

TesseractConfig local_config = {
stim_dem, det_beam, beam_climbing, no_revisit_dets, verbose,
merge_errors, pqlimit, det_orders, det_penalty, create_visualization};
TesseractDecoder decoder(local_config);

// Calculate expected number of bytes per shot for detectors and observables.
Expand Down Expand Up @@ -254,14 +304,17 @@ void pybind_sinter_compat(py::module& root) {
.def(py::init<>(), R"pbdoc(
Initializes a new TesseractSinterDecoder instance with a default TesseractConfig.
)pbdoc")
.def(py::init<const TesseractConfig&>(), 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(
py::init<int, bool, bool, bool, bool, size_t, double, bool, size_t, DetOrder, uint64_t>(),
py::arg("det_beam") = DEFAULT_DET_BEAM, py::arg("beam_climbing") = false,
py::arg("no_revisit_dets") = true, py::arg("verbose") = false,
py::arg("merge_errors") = true, py::arg("pqlimit") = DEFAULT_PQLIMIT,
py::arg("det_penalty") = 0.0, py::arg("create_visualization") = false,
py::arg("num_det_orders") = 0, py::arg("det_order_method") = DetOrder::DetBFS,
py::arg("seed") = 2384753,
R"pbdoc(
Initializes a new TesseractSinterDecoder instance with custom TesseractConfig parameters.
)pbdoc")
.def("compile_decoder_for_dem", &TesseractSinterDecoder::compile_decoder_for_dem,
py::kw_only(), py::arg("dem"),
R"pbdoc(
Expand All @@ -286,34 +339,36 @@ void pybind_sinter_compat(py::module& root) {
bit-packed observable predictions will be written.
:param tmp_dir: A temporary directory path. (Currently unused, but required by API)
)pbdoc")
.def_readwrite("det_beam", &TesseractSinterDecoder::det_beam)
.def_readwrite("beam_climbing", &TesseractSinterDecoder::beam_climbing)
.def_readwrite("no_revisit_dets", &TesseractSinterDecoder::no_revisit_dets)
.def_readwrite("verbose", &TesseractSinterDecoder::verbose)
.def_readwrite("merge_errors", &TesseractSinterDecoder::merge_errors)
.def_readwrite("pqlimit", &TesseractSinterDecoder::pqlimit)
.def_readwrite("det_penalty", &TesseractSinterDecoder::det_penalty)
.def_readwrite("create_visualization", &TesseractSinterDecoder::create_visualization)
.def_readwrite("num_det_orders", &TesseractSinterDecoder::num_det_orders)
.def_readwrite("det_order_method", &TesseractSinterDecoder::det_order_method)
.def_readwrite("seed", &TesseractSinterDecoder::seed)
.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")
.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.verbose, self.config.merge_errors,
self.config.pqlimit, self.config.det_orders,
self.config.det_penalty, self.config.create_visualization);
return py::make_tuple(self.det_beam, self.beam_climbing, self.no_revisit_dets,
self.verbose, self.merge_errors, self.pqlimit, self.det_penalty,
self.create_visualization, self.num_det_orders,
self.det_order_method, self.seed);
},
[](py::tuple t) { // __setstate__
if (t.size() != 10) {
if (t.size() != 11) {
throw std::runtime_error("Invalid state for TesseractSinterDecoder!");
}
TesseractConfig config;
config.dem = stim::DetectorErrorModel(t[0].cast<std::string>());
config.det_beam = t[1].cast<int>();
config.beam_climbing = t[2].cast<bool>();
config.no_revisit_dets = t[3].cast<bool>();
config.verbose = t[4].cast<bool>();
config.merge_errors = t[5].cast<bool>();
config.pqlimit = t[6].cast<size_t>();
config.det_orders = t[7].cast<std::vector<std::vector<size_t>>>();
config.det_penalty = t[8].cast<double>();
config.create_visualization = t[9].cast<bool>();
return TesseractSinterDecoder(config);
return TesseractSinterDecoder(
t[0].cast<int>(), t[1].cast<bool>(), t[2].cast<bool>(), t[3].cast<bool>(),
t[4].cast<bool>(), t[5].cast<size_t>(), t[6].cast<double>(), t[7].cast<bool>(),
t[8].cast<size_t>(), t[9].cast<DetOrder>(), t[10].cast<uint64_t>());
}));

// Add a function to create a dictionary of custom decoders
Expand All @@ -322,6 +377,16 @@ void pybind_sinter_compat(py::module& root) {
[]() -> py::object {
auto result = py::dict();
result["tesseract"] = TesseractSinterDecoder{};
result["tesseract-short-beam"] = TesseractSinterDecoder(
/*det_beam=*/10, /*beam_climbing=*/false, /*no_revisit_dets=*/true,
/*verbose=*/false, /*merge_errors=*/true, /*pqlimit=*/DEFAULT_PQLIMIT,
/*det_penalty=*/0.0, /*create_visualization=*/false,
/*num_det_orders=*/0, /*det_order_method=*/DetOrder::DetBFS, /*seed=*/2384753);
result["tesseract-long-beam"] = TesseractSinterDecoder(
/*det_beam=*/1000, /*beam_climbing=*/false, /*no_revisit_dets=*/true,
/*verbose=*/false, /*merge_errors=*/true, /*pqlimit=*/DEFAULT_PQLIMIT,
/*det_penalty=*/0.0, /*create_visualization=*/false,
/*num_det_orders=*/0, /*det_order_method=*/DetOrder::DetBFS, /*seed=*/2384753);
return result;
},
R"pbdoc(
Expand Down