-
Notifications
You must be signed in to change notification settings - Fork 18
Description
Describe the issue
Currently, the TesseractSinterDecoder has the option to be configured without providing a detector error model, but doing so does not work if (1) det_orders are built based on the DEM when configuring the sinter decoder and (2) a different DEM is then used to decode. It would be nice to have better compatibility with sinter by fixing this issue, as well as updating the documentation with the fixed method.
For example the following code:
import sinter
import stim
from tesseract_decoder.tesseract_sinter_compat import (
make_tesseract_sinter_decoders_dict,
TesseractSinterDecoder,
)
import tesseract_decoder
from tesseract_decoder.tesseract import TesseractConfig
if __name__ == "__main__":
# 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)
]
dem = tasks[0].circuit.detector_error_model()
det_orders = tesseract_decoder.utils.build_det_orders(
dem=dem,
num_det_orders=21,
method=tesseract_decoder.utils.DetOrder.DetIndex,
seed=2384753,
)
tesseract_config = TesseractConfig(
dem=dem,
det_beam=20,
beam_climbing=True,
pqlimit=1_000_000,
no_revisit_dets=True,
det_orders=det_orders,
)
sinter_decoder = TesseractSinterDecoder(config=tesseract_config)
# Use sinter.collect to run the decoding task.
all_results = sinter.collect(
num_workers=1,
tasks=tasks,
decoders=["tesseract-long-beam"],
max_shots=1000,
custom_decoders={"tesseract-long-beam": sinter_decoder},
)
for results in all_results:
print(f"d={results.json_metadata['d']}")
# Print a summary of the decoding results.
print(f" Shots run: {results.shots}")
print(f" Observed errors: {results.errors}")
print(f" Logical error rate: {results.errors / results.shots}")
throws the exception:
(tesseract) oscarhiggott@oscarhiggott:~/Documents/software/tesseract/issues/sinter-dem-issue$ python sinter_dem_issue.py
ValueError: Each detector order list must have a size equal to the number of detectors.
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/usr/local/google/home/oscarhiggott/Documents/software/tesseract/issues/sinter-dem-issue/sinter_dem_issue.py", line 49, in <module>
all_results = sinter.collect(
num_workers=1,
...<3 lines>...
custom_decoders={"tesseract-long-beam": sinter_decoder},
)
File "/usr/local/google/home/oscarhiggott/.virtualenvs/tesseract/lib/python3.13/site-packages/sinter/_collection/_collection.py", line 404, in collect
for progress in iter_collect(
~~~~~~~~~~~~^
num_workers=num_workers,
^^^^^^^^^^^^^^^^^^^^^^^^
...<13 lines>...
allowed_cpu_affinity_ids=allowed_cpu_affinity_ids,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
):
^
File "/usr/local/google/home/oscarhiggott/.virtualenvs/tesseract/lib/python3.13/site-packages/sinter/_collection/_collection.py", line 226, in iter_collect
manager.process_message()
~~~~~~~~~~~~~~~~~~~~~~~^^
File "/usr/local/google/home/oscarhiggott/.virtualenvs/tesseract/lib/python3.13/site-packages/sinter/_collection/_collection_manager.py", line 385, in process_message
raise RuntimeError(f'Worker failed: traceback={traceback}') from ex
RuntimeError: Worker failed: traceback=Traceback (most recent call last):
File "/usr/local/google/home/oscarhiggott/.virtualenvs/tesseract/lib/python3.13/site-packages/sinter/_collection/_collection_worker_state.py", line 243, in run_message_loop
num_messages_processed = self.process_messages()
File "/usr/local/google/home/oscarhiggott/.virtualenvs/tesseract/lib/python3.13/site-packages/sinter/_collection/_collection_worker_state.py", line 182, in process_messages
self.change_job(new_task=new_task, new_collection_options=new_collection_options)
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/google/home/oscarhiggott/.virtualenvs/tesseract/lib/python3.13/site-packages/sinter/_collection/_collection_worker_state.py", line 145, in change_job
self.compiled_sampler = self.sampler.compiled_sampler_for_task(self.current_task)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
File "/usr/local/google/home/oscarhiggott/.virtualenvs/tesseract/lib/python3.13/site-packages/sinter/_collection/_sampler_ramp_throttled.py", line 27, in compiled_sampler_for_task
compiled_sub_sampler = self.sub_sampler.compiled_sampler_for_task(task)
File "/usr/local/google/home/oscarhiggott/.virtualenvs/tesseract/lib/python3.13/site-packages/sinter/_collection/_mux_sampler.py", line 30, in compiled_sampler_for_task
return self._resolve_sampler(task.decoder).compiled_sampler_for_task(task)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
File "/usr/local/google/home/oscarhiggott/.virtualenvs/tesseract/lib/python3.13/site-packages/sinter/_decoding/_stim_then_decode_sampler.py", line 40, in compiled_sampler_for_task
return _CompiledStimThenDecodeSampler(
decoder=self.decoder,
...<3 lines>...
tmp_dir=self.tmp_dir,
)
File "/usr/local/google/home/oscarhiggott/.virtualenvs/tesseract/lib/python3.13/site-packages/sinter/_decoding/_stim_then_decode_sampler.py", line 161, in __init__
self.compiled_decoder = _compile_decoder_with_disk_fallback(decoder, task, tmp_dir)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/google/home/oscarhiggott/.virtualenvs/tesseract/lib/python3.13/site-packages/sinter/_decoding/_stim_then_decode_sampler.py", line 142, in _compile_decoder_with_disk_fallback
return decoder.compile_decoder_for_dem(dem=task.detector_error_model)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: Each detector order list must have a size equal to the number of detectors.
The reason for this is that tesseract_decoder.utils.build_det_orders has to be run when generating the config (the output of this method is given to the config) which means the actual config is only suitable for one dem, whereas sinter expects a sinter decoder to be useable for any dem. For example, notice that this code works if max_distance is set to be equal to min_distance.
Arguably, it would be more natural if the arguments to build_det_orders were given to the config (i.e. num_det_orders, method, seed, perhaps as a struct) and then build_det_orders can then be used within the constructor for both TesseractDecoder and TesseractSinterDecoder. However, this fix might be too disruptive, especially if backward compatibility is desired.
Another option, which is probably the best approach to fix this issue for now, is just to refactor how the TesseractSinterDecoder is constructed. The following is some python glue code I wrote that fixes the problem, however it would probably be better if this was implemented in C++ using pybind11 to expose the method, with both TesseractSinterDecoder and make_tesseract_sinter_decoders_dict available in tesseract_decoder.tesseract_sinter_compat:
from sinter._decoding._decoding_decoder_class import Decoder, CompiledDecoder
import numpy as np
import stim
from tesseract_decoder import tesseract
import tesseract_decoder
class TesseractCompiledDecoder(CompiledDecoder):
def __init__(self, decoder: "tesseract.TesseractDecoder"):
self.decoder = decoder
def decode_shots_bit_packed(
self,
*,
bit_packed_detection_event_data: "np.ndarray",
) -> "np.ndarray":
unpacked_bits = np.unpackbits(
bit_packed_detection_event_data, axis=1, bitorder="little"
)
predictions = self.decoder.decode_batch(
unpacked_bits[:, : self.decoder.config.dem.num_detectors]
)
return np.packbits(predictions, bitorder="little", axis=1)
class TesseractSinterDecoder(Decoder):
def __init__(
self,
pqlimit: int,
det_beam: int,
beam_climbing: bool,
no_revisit_dets: bool,
num_det_orders: int,
det_order: tesseract_decoder.utils.DetOrder,
seed: int = 2384753,
):
self.pqlimit = pqlimit
self.det_beam = det_beam
self.beam_climbing = beam_climbing
self.no_revisit_dets = no_revisit_dets
self.num_det_orders = num_det_orders
self.det_order = det_order
self.seed = seed
def compile_decoder_for_dem(
self, *, dem: "stim.DetectorErrorModel"
) -> CompiledDecoder:
det_orders = tesseract_decoder.utils.build_det_orders(
dem=dem,
num_det_orders=self.num_det_orders,
method=self.det_order,
seed=self.seed,
)
tesseract_config = tesseract.TesseractConfig(
dem=dem,
det_beam=self.det_beam,
beam_climbing=self.beam_climbing,
pqlimit=self.pqlimit,
no_revisit_dets=self.no_revisit_dets,
det_orders=det_orders,
)
decoder = tesseract_config.compile_decoder()
return TesseractCompiledDecoder(decoder=decoder)
def make_tesseract_sinter_decoders_dict() -> dict[str, Decoder]:
return {
"tesseract-long-beam": TesseractSinterDecoder(
pqlimit=1_000_000,
det_beam=20,
beam_climbing=True,
num_det_orders=21,
det_order=tesseract_decoder.utils.DetOrder.DetIndex,
no_revisit_dets=True,
),
"tesseract-short-beam": TesseractSinterDecoder(
pqlimit=200_000,
det_beam=15,
beam_climbing=True,
num_det_orders=16,
det_order=tesseract_decoder.utils.DetOrder.DetIndex,
no_revisit_dets=True,
),
}
if __name__ == "__main__":
circuit = stim.Circuit.generated(
"surface_code:rotated_memory_x",
distance=5,
rounds=5,
after_clifford_depolarization=0.003,
)
dem = circuit.detector_error_model(decompose_errors=True)
decoders = make_tesseract_sinter_decoders_dict()
tesseract_long_beam_decoder = decoders["tesseract-long-beam"]
compiled_long_beam_decoder = tesseract_long_beam_decoder.compile_decoder_for_dem(
dem=dem
)
num_shots = 10000
shots, actual_obs = circuit.compile_detector_sampler().sample(
shots=num_shots, bit_packed=True, separate_observables=True
)
predicted_obs_long_beam = compiled_long_beam_decoder.decode_shots_bit_packed(
bit_packed_detection_event_data=shots
)
num_mistakes_long_beam = np.sum(
np.any(predicted_obs_long_beam != actual_obs, axis=1)
)
print(f"num_errors / num_shots (Tesseract long beam): {num_mistakes_long_beam} / {num_shots}")
What version of the software are you using?
tesseract_decoder==0.1.1.dev20250930021520
sinter==1.15.0
numpy==2.0.0
How can the issue be reproduced?
No response