Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
132 changes: 132 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,138 @@ 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/Stim/tree/main/glue/sample) workflows. Sinter is a tool for running and organizing quantum error correction simulations.

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 make_tesseract_sinter_decoders_dict, TesseractSinterDecoder
import tesseract_decoder

if __name__ == "__main__":
# Define a list of Sinter task(s) with different circuits/decoders.
tasks = []
# Depolarizing noise probability.
p = 0.005
# These are the sensible defaults given by make_tesseract_sinter_decoders_dict().
# Note that `tesseract-short-beam` and `tesseract-long-beam` are the two sets of parameters used in the [Tesseract paper](https://arxiv.org/pdf/2503.10988).
decoders = ['tesseract', 'tesseract-long-beam', 'tesseract-short-beam']
decoder_dict = make_tesseract_sinter_decoders_dict()
# You can also make your own custom Tesseract Decoder to-be-used with Sinter.
decoders.append('custom-tesseract-decoder')
decoder_dict['custom-tesseract-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,
)

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

# Collect decoding outcomes per task from Sinter.
results = sinter.collect(
num_workers=8,
tasks=tasks,
max_shots=10_000,
decoders=decoders,
custom_decoders=decoder_dict,
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': 3, 'decoder': 'tesseract'}
# Shots run: 10000
# Observed errors: 48
# Logical error rate: 0.0048
# task metadata = {'d': 3, 'decoder': 'custom-tesseract-decoder'}
# Shots run: 10000
# Observed errors: 64
# Logical error rate: 0.0064
# task metadata = {'d': 5, 'decoder': 'tesseract-short-beam'}
# Shots run: 10000
# Observed errors: 13
# Logical error rate: 0.0013
# task metadata = {'d': 5, 'decoder': 'custom-tesseract-decoder'}
# Shots run: 10000
# Observed errors: 12
# Logical error rate: 0.0012
# task metadata = {'d': 3, 'decoder': 'tesseract-long-beam'}
# Shots run: 10000
# Observed errors: 42
# Logical error rate: 0.0042
# task metadata = {'d': 3, 'decoder': 'tesseract-short-beam'}
# Shots run: 10000
# Observed errors: 39
# Logical error rate: 0.0039
# task metadata = {'d': 5, 'decoder': 'tesseract'}
# Shots run: 10000
# Observed errors: 15
# Logical error rate: 0.0015
# task metadata = {'d': 5, 'decoder': 'tesseract-long-beam'}
# Shots run: 10000
# Observed errors: 13
# Logical error rate: 0.0013
# task metadata = {'d': 7, 'decoder': 'tesseract'}
# Shots run: 10000
# Observed errors: 5
# Logical error rate: 0.0005
# task metadata = {'d': 7, 'decoder': 'tesseract-long-beam'}
# Shots run: 10000
# Observed errors: 1
# Logical error rate: 0.0001
# task metadata = {'d': 7, 'decoder': 'tesseract-short-beam'}
# Shots run: 10000
# Observed errors: 3
# Logical error rate: 0.0003
# task metadata = {'d': 7, 'decoder': 'custom-tesseract-decoder'}
# Shots run: 10000
# Observed errors: 3
# Logical error rate: 0.0003
```
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks great, just a nit: maybe we could just print out the csv instead as that will take up less vertical space on the README:

Suggested change
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': 3, 'decoder': 'tesseract'}
# Shots run: 10000
# Observed errors: 48
# Logical error rate: 0.0048
# task metadata = {'d': 3, 'decoder': 'custom-tesseract-decoder'}
# Shots run: 10000
# Observed errors: 64
# Logical error rate: 0.0064
# task metadata = {'d': 5, 'decoder': 'tesseract-short-beam'}
# Shots run: 10000
# Observed errors: 13
# Logical error rate: 0.0013
# task metadata = {'d': 5, 'decoder': 'custom-tesseract-decoder'}
# Shots run: 10000
# Observed errors: 12
# Logical error rate: 0.0012
# task metadata = {'d': 3, 'decoder': 'tesseract-long-beam'}
# Shots run: 10000
# Observed errors: 42
# Logical error rate: 0.0042
# task metadata = {'d': 3, 'decoder': 'tesseract-short-beam'}
# Shots run: 10000
# Observed errors: 39
# Logical error rate: 0.0039
# task metadata = {'d': 5, 'decoder': 'tesseract'}
# Shots run: 10000
# Observed errors: 15
# Logical error rate: 0.0015
# task metadata = {'d': 5, 'decoder': 'tesseract-long-beam'}
# Shots run: 10000
# Observed errors: 13
# Logical error rate: 0.0013
# task metadata = {'d': 7, 'decoder': 'tesseract'}
# Shots run: 10000
# Observed errors: 5
# Logical error rate: 0.0005
# task metadata = {'d': 7, 'decoder': 'tesseract-long-beam'}
# Shots run: 10000
# Observed errors: 1
# Logical error rate: 0.0001
# task metadata = {'d': 7, 'decoder': 'tesseract-short-beam'}
# Shots run: 10000
# Observed errors: 3
# Logical error rate: 0.0003
# task metadata = {'d': 7, 'decoder': 'custom-tesseract-decoder'}
# Shots run: 10000
# Observed errors: 3
# Logical error rate: 0.0003
```
# Print samples as CSV data.
print(sinter.CSV_HEADER)
for sample in results:
print(sample.to_csv_line())

should get something like:

     shots,    errors,  discards, seconds,decoder,strong_id,json_metadata,custom_counts
     10000,        42,         0,   0.071,tesseract,1b3fce6286e438f38c00c8f6a5005947373515ab08e6446a7dd9ecdbef12d4cc,"{""d"":3,""decoder"":""tesseract""}",
     10000,        49,         0,   0.546,custom-tesseract-decoder,7b082bec7541be858e239d7828a432e329cd448356bbdf051b8b8aa76c86625a,"{""d"":3,""decoder"":""custom-tesseract-decoder""}",
     10000,        13,         0,    7.64,tesseract-long-beam,217a3542f56319924576658a6da7081ea2833f5167cf6d77fbc7071548e386a9,"{""d"":5,""decoder"":""tesseract-long-beam""}",
     10000,        42,         0,   0.743,tesseract-short-beam,cf4a4b0ce0e4c7beec1171f58eddffe403ed7359db5016fca2e16174ea577057,"{""d"":3,""decoder"":""tesseract-short-beam""}",
     10000,        34,         0,   0.924,tesseract-long-beam,8cfa0f2e4061629e13bc98fe213285dc00eb90f21bba36e08c76bcdf213a1c09,"{""d"":3,""decoder"":""tesseract-long-beam""}",
     10000,        10,         0,   0.439,tesseract,8274ea5ffec15d6e71faed5ee1057cdd7e497cbaee4c6109784f8a74669d7f96,"{""d"":5,""decoder"":""tesseract""}",
     10000,         8,         0,    3.93,custom-tesseract-decoder,8e4f5ab5dde00fec74127eea39ea52d5a98ae6ccfc277b5d9be450f78acc1c45,"{""d"":5,""decoder"":""custom-tesseract-decoder""}",
     10000,        10,         0,    5.74,tesseract-short-beam,bf696535d62a25720c3a0c624ec5624002efe3f6cb0468963eee702efb48abc1,"{""d"":5,""decoder"":""tesseract-short-beam""}",
     10000,         5,         0,    1.27,tesseract,3f94c61f1503844df6cf0d200b74ac01bfbc5e29e70cedbfc2faad67047e7887,"{""d"":7,""decoder"":""tesseract""}",
     10000,         4,         0,    25.0,tesseract-long-beam,4d510f0acf511e24a833a93c956b683346696d8086866fadc73063fb09014c23,"{""d"":7,""decoder"":""tesseract-long-beam""}",
     10000,         1,         0,    18.6,tesseract-short-beam,75782ce4593022fcedad4c73104711f05c9c635db92869531f78da336945b121,"{""d"":7,""decoder"":""tesseract-short-beam""}",
     10000,         4,         0,    11.6,custom-tesseract-decoder,48f256a28fff47c58af7bffdf98fdee1d41a721751ee965c5d3c5712ac795dc8,"{""d"":7,""decoder"":""custom-tesseract-decoder""}",


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:

```bash
sinter collect \
--circuits "example_circuit.stim" \
--decoders tesseract \
--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`.

## 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
104 changes: 72 additions & 32 deletions src/py/tesseract_sinter_compat_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import shutil
from sinter._decoding._decoding import sample_decode

from src.tesseract_decoder import tesseract_sinter_compat as tesseract_module
from src.tesseract_decoder import TesseractSinterDecoder, make_tesseract_sinter_decoders_dict
from src import tesseract_decoder
import sinter

Expand All @@ -29,7 +29,7 @@ def test_tesseract_sinter_obj_exists():
Sanity check to ensure the decoder object exists and has the required methods.
"""

decoder = tesseract_module.TesseractSinterDecoder()
decoder = TesseractSinterDecoder()
assert hasattr(decoder, 'compile_decoder_for_dem')
assert hasattr(decoder, 'decode_via_files')

Expand All @@ -51,11 +51,11 @@ 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 = TesseractSinterDecoder(
verbose=True,
)
else:
decoder = tesseract_module.TesseractSinterDecoder()
decoder = TesseractSinterDecoder()

compiled_decoder = decoder.compile_decoder_for_dem(dem=dem)

Expand Down Expand Up @@ -83,7 +83,7 @@ def test_decode_shots_bit_packed():
error(0.1) D1 D2 L1
""")

decoder = tesseract_module.TesseractSinterDecoder()
decoder = TesseractSinterDecoder()
compiled_decoder = decoder.compile_decoder_for_dem(dem=dem)

num_shots = 1
Expand Down Expand Up @@ -119,7 +119,7 @@ def test_decode_shots_bit_packed_multi_shot():
error(0.1) D1 D2 L1
""")

decoder = tesseract_module.TesseractSinterDecoder()
decoder = TesseractSinterDecoder()
compiled_decoder = decoder.compile_decoder_for_dem(dem=dem)

num_shots = 3
Expand Down Expand Up @@ -184,7 +184,7 @@ def test_decode_via_files_sanity_check():
with open(dets_in_path, 'wb') as f:
f.write(detection_events.tobytes())

tesseract_module.TesseractSinterDecoder().decode_via_files(
TesseractSinterDecoder().decode_via_files(
num_shots=num_shots,
num_dets=dem.num_detectors,
num_obs=dem.num_observables,
Expand Down Expand Up @@ -247,11 +247,11 @@ 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 = TesseractSinterDecoder(
verbose=True,
)
else:
decoder = tesseract_module.TesseractSinterDecoder()
decoder = TesseractSinterDecoder()

decoder.decode_via_files(
num_shots=num_shots,
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 @@ -332,7 +332,7 @@ def test_decode_via_files_multi_shot():
with open(dets_in_path, 'wb') as f:
f.write(detection_events_np.tobytes())

tesseract_module.TesseractSinterDecoder().decode_via_files(
TesseractSinterDecoder().decode_via_files(
num_shots=num_shots,
num_dets=num_detectors,
num_obs=dem.num_observables,
Expand Down Expand Up @@ -378,7 +378,7 @@ def test_sinter_decode_repetition_code():
dem_path=None,
num_shots=1000,
decoder="tesseract",
custom_decoders=tesseract_module.make_tesseract_sinter_decoders_dict(),
custom_decoders=make_tesseract_sinter_decoders_dict(),
)
assert result.discards == 0
assert 0 <= result.errors <= 100
Expand All @@ -402,7 +402,7 @@ def test_sinter_decode_surface_code():
dem_obj=circuit.detector_error_model(decompose_errors=True),
dem_path=None,
decoder="tesseract",
custom_decoders=tesseract_module.make_tesseract_sinter_decoders_dict(),
custom_decoders=make_tesseract_sinter_decoders_dict(),
)
assert result.discards == 0
assert 0 <= result.errors <= 50
Expand All @@ -421,7 +421,7 @@ def test_sinter_empty():
dem_path=None,
num_shots=1000,
decoder="tesseract",
custom_decoders=tesseract_module.make_tesseract_sinter_decoders_dict(),
custom_decoders=make_tesseract_sinter_decoders_dict(),
)
assert result.discards == 0
assert result.shots == 1000
Expand All @@ -444,7 +444,7 @@ def test_sinter_no_observables():
dem_path=None,
num_shots=1000,
decoder="tesseract",
custom_decoders=tesseract_module.make_tesseract_sinter_decoders_dict(),
custom_decoders=make_tesseract_sinter_decoders_dict(),
)
assert result.discards == 0
assert result.shots == 1000
Expand All @@ -468,7 +468,7 @@ def test_sinter_invincible_observables():
dem_path=None,
num_shots=1000,
decoder="tesseract",
custom_decoders=tesseract_module.make_tesseract_sinter_decoders_dict(),
custom_decoders=make_tesseract_sinter_decoders_dict(),
)
assert result.discards == 0
assert result.shots == 1000
Expand Down Expand Up @@ -497,7 +497,7 @@ def test_sinter_detector_counting():
num_shots=10000,
decoder="tesseract",
count_detection_events=True,
custom_decoders=tesseract_module.make_tesseract_sinter_decoders_dict(),
custom_decoders=make_tesseract_sinter_decoders_dict(),
)
assert result.discards == 0
assert result.custom_counts['detectors_checked'] == 20000
Expand All @@ -513,7 +513,7 @@ def test_full_scale():
tasks=[sinter.Task(circuit=stim.Circuit())],
decoders=["tesseract"],
max_shots=1000,
custom_decoders=tesseract_module.make_tesseract_sinter_decoders_dict(),
custom_decoders=make_tesseract_sinter_decoders_dict(),
)
assert result.discards == 0
assert result.shots == 1000
Expand All @@ -535,7 +535,7 @@ def test_full_scale_one_worker():
tasks=[sinter.Task(circuit=circuit)],
decoders=["tesseract"],
max_shots=1000,
custom_decoders=tesseract_module.make_tesseract_sinter_decoders_dict(),
custom_decoders=make_tesseract_sinter_decoders_dict(),
)

assert result.discards == 0
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 = 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=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__]))
Loading