Skip to content

TesseractSinterDecoder requires DEM to be configured correctly #129

@oscarhiggott

Description

@oscarhiggott

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions