Skip to content

Commit feb094c

Browse files
authored
Expose error model sparsification to python module, update sinter decoders dict (#257)
1 parent e83a468 commit feb094c

15 files changed

Lines changed: 790 additions & 92 deletions

CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ FetchContent_MakeAvailable(stim)
1919
# HiGHS
2020
FetchContent_Declare(
2121
highs
22-
URL https://github.com/ERGO-Code/HiGHS/archive/refs/tags/v1.9.0.tar.gz
23-
URL_HASH SHA256=dff575df08d88583c109702c7c5c75ff6e51611e6eacca8b5b3fdfba8ecc2cb4
22+
URL https://github.com/ERGO-Code/HiGHS/archive/refs/tags/v1.14.0.tar.gz
23+
URL_HASH SHA256=05931e8dd8c8cac514da8297003c31a206a0004d542b7da500810b85c87c20b9
2424
)
2525
FetchContent_MakeAvailable(highs)
2626

README.md

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ Example with Advanced Options:
113113
--pqlimit 1000000 \
114114
--no-revisit-dets \
115115
--det-order-seed 232852747 \
116-
--det-order-index --num-det-orders 24 \
116+
--num-det-orders 24 \
117117
--circuit circuit_file.stim \
118118
--sample-seed 232856747 \
119119
--sample-num-shots 10000 \
@@ -159,6 +159,35 @@ Here are some tips for improving performance:
159159
* *At most two errors per detector*: enable `--at-most-two-errors-per-detector` to improve
160160
performance.
161161
* *Priority Queue limit*: use `--pqlimit` to limit the size of the priority queue.
162+
* *Error sparsification*: enable `--sparsify-errors` to always keep low-degree errors while
163+
selectively reactivating high-degree errors per shot. This can improve runtime on DEMs with
164+
many high-degree errors, at the cost of a tunable accuracy/speed tradeoff.
165+
166+
Example with error sparsification:
167+
168+
```bash
169+
./tesseract \
170+
--circuit circuit_file.stim \
171+
--sample-num-shots 10000 \
172+
--beam 20 \
173+
--beam-climbing \
174+
--sparsify-errors \
175+
--sparsify-base-degree 3 \
176+
--print-stats
177+
```
178+
179+
`--sparsify-base-degree K` is required when `--sparsify-errors` is enabled. Errors touching at most
180+
`K` detectors are always active. Errors above `K` are optional and are ranked per shot by overlap
181+
with the fired detectors. In the surface code (or other 'mostly graphlike' codes) try K = 2. In the
182+
color code or bivariate bicycle codes, try K = 3. In general, it is recommended to set K to the number
183+
of activated detectors created by a single data qubit error in the bulk, restricting to X or Z errors
184+
only for CSS codes.
185+
186+
`--sparsify-reactivate-limit M` caps the number of optional high-degree errors reactivated per
187+
shot. If omitted, Tesseract uses `round((4.5^(K - 2) / 3) * num_detectors)`.
188+
189+
`--sparsify-max-degree D` optionally excludes optional errors above degree `D`. If omitted, optional
190+
errors are not capped by degree.
162191

163192
### Output Formats
164193

@@ -197,8 +226,21 @@ dem = stim.DetectorErrorModel("""
197226
# 2. Create the decoder configuration
198227
config = tesseract.TesseractConfig(dem=dem, det_beam=50)
199228

229+
# To enable sparse activation for high-degree DEMs:
230+
config = tesseract.TesseractConfig(
231+
dem=dem,
232+
det_beam=50,
233+
sparsify_errors=True,
234+
sparsify_base_degree=3,
235+
sparsify_reactivate_limit=-1, # Use the built-in heuristic, clamped to error count.
236+
)
237+
200238
# 3. Create a decoder instance
201239
decoder = config.compile_decoder()
240+
print(
241+
"Resolved sparsify reactivation limit:",
242+
decoder.config.sparsify_reactivate_limit,
243+
)
202244

203245
# 4. Simulate detector outcomes
204246
syndrome = np.array([0, 1, 1], dtype=bool)
@@ -235,7 +277,13 @@ if __name__ == "__main__":
235277
p = 0.005
236278
# These are the sensible defaults given by make_tesseract_sinter_decoders_dict().
237279
# 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).
238-
decoders = ['tesseract', 'tesseract-long-beam', 'tesseract-short-beam']
280+
decoders = [
281+
'tesseract',
282+
'tesseract-long-beam',
283+
'tesseract-short-beam',
284+
'tesseract-long-beam-sparsify-color-code-like',
285+
'tesseract-short-beam-sparsify-surface-code-like',
286+
]
239287
decoder_dict = make_tesseract_sinter_decoders_dict()
240288
# You can also make your own custom Tesseract Decoder to-be-used with Sinter.
241289
decoders.append('custom-tesseract-decoder')
@@ -248,6 +296,9 @@ if __name__ == "__main__":
248296
num_det_orders=5,
249297
det_order_method=tesseract_decoder.utils.DetOrder.DetIndex,
250298
seed=2384753,
299+
sparsify_errors=True,
300+
sparsify_base_degree=3,
301+
sparsify_reactivate_limit=-1,
251302
)
252303

253304
for distance in [3, 5, 7]:
@@ -285,13 +336,16 @@ should get something like:
285336
10000, 42, 0, 0.071,tesseract,1b3fce6286e438f38c00c8f6a5005947373515ab08e6446a7dd9ecdbef12d4cc,"{""d"":3,""decoder"":""tesseract""}",
286337
10000, 49, 0, 0.546,custom-tesseract-decoder,7b082bec7541be858e239d7828a432e329cd448356bbdf051b8b8aa76c86625a,"{""d"":3,""decoder"":""custom-tesseract-decoder""}",
287338
10000, 13, 0, 7.64,tesseract-long-beam,217a3542f56319924576658a6da7081ea2833f5167cf6d77fbc7071548e386a9,"{""d"":5,""decoder"":""tesseract-long-beam""}",
339+
10000, 14, 0, 4.12,tesseract-long-beam-sparsify-color-code-like,14fa5f9f08381d760f6c1f59805b75f2c70cfb83e50d9f1f40d92820a20eeb13,"{""d"":5,""decoder"":""tesseract-long-beam-sparsify-color-code-like""}",
288340
10000, 42, 0, 0.743,tesseract-short-beam,cf4a4b0ce0e4c7beec1171f58eddffe403ed7359db5016fca2e16174ea577057,"{""d"":3,""decoder"":""tesseract-short-beam""}",
289341
10000, 34, 0, 0.924,tesseract-long-beam,8cfa0f2e4061629e13bc98fe213285dc00eb90f21bba36e08c76bcdf213a1c09,"{""d"":3,""decoder"":""tesseract-long-beam""}",
342+
10000, 35, 0, 0.681,tesseract-long-beam-sparsify-color-code-like,f41bdb1bde3f5cf4893a9a9e33fc7d4c47d742f22b13dfec9195347e780119bc,"{""d"":3,""decoder"":""tesseract-long-beam-sparsify-color-code-like""}",
290343
10000, 10, 0, 0.439,tesseract,8274ea5ffec15d6e71faed5ee1057cdd7e497cbaee4c6109784f8a74669d7f96,"{""d"":5,""decoder"":""tesseract""}",
291344
10000, 8, 0, 3.93,custom-tesseract-decoder,8e4f5ab5dde00fec74127eea39ea52d5a98ae6ccfc277b5d9be450f78acc1c45,"{""d"":5,""decoder"":""custom-tesseract-decoder""}",
292345
10000, 10, 0, 5.74,tesseract-short-beam,bf696535d62a25720c3a0c624ec5624002efe3f6cb0468963eee702efb48abc1,"{""d"":5,""decoder"":""tesseract-short-beam""}",
293346
10000, 5, 0, 1.27,tesseract,3f94c61f1503844df6cf0d200b74ac01bfbc5e29e70cedbfc2faad67047e7887,"{""d"":7,""decoder"":""tesseract""}",
294347
10000, 4, 0, 25.0,tesseract-long-beam,4d510f0acf511e24a833a93c956b683346696d8086866fadc73063fb09014c23,"{""d"":7,""decoder"":""tesseract-long-beam""}",
348+
10000, 4, 0, 14.8,tesseract-long-beam-sparsify-color-code-like,80868acc6e43c62cb73b242b66ae27d3ea08fe970ea879db5a8425c2454fc8a1,"{""d"":7,""decoder"":""tesseract-long-beam-sparsify-color-code-like""}",
295349
10000, 1, 0, 18.6,tesseract-short-beam,75782ce4593022fcedad4c73104711f05c9c635db92869531f78da336945b121,"{""d"":7,""decoder"":""tesseract-short-beam""}",
296350
10000, 4, 0, 11.6,custom-tesseract-decoder,48f256a28fff47c58af7bffdf98fdee1d41a721751ee965c5d3c5712ac795dc8,"{""d"":7,""decoder"":""custom-tesseract-decoder""}",
297351
```
@@ -348,8 +402,27 @@ tesseract_config = tesseract.TesseractConfig(
348402
no_revisit_dets=True,
349403
)
350404
```
351-
For `det_order`, you can use two other options of `DetIndex` and `DetCoordinate` as well.
405+
`DetIndex` is the default detector ordering. You can also pass `DetBFS` or `DetCoordinate`
406+
explicitly.
352407
These values balance decoding speed and accuracy across the benchmarks reported in the paper and can be adjusted for specific use cases.
408+
409+
The Sinter decoder dictionary also provides sparsified variants:
410+
`tesseract-long-beam-sparsify-color-code-like`,
411+
`tesseract-long-beam-sparsify-surface-code-like`,
412+
`tesseract-short-beam-sparsify-color-code-like`, and
413+
`tesseract-short-beam-sparsify-surface-code-like`.
414+
415+
As a quick rule of thumb, use the non-sparsified decoders as the safest baseline. Use the
416+
`surface-code-like` variants for surface-code-like or mostly graphlike DEMs, and use the
417+
`color-code-like` variants for color-code,
418+
bivariate-bicycle-code, or other DEMs where a typical bulk data error activates about three
419+
detectors. Within either family, prefer the long-beam variants when accuracy matters more and the
420+
short-beam variants when runtime matters more. See the
421+
[Performance Optimization](#performance-optimization) section for the full sparsification details.
422+
423+
Equivalent Python configs can enable sparsification with `sparsify_errors=True`,
424+
`sparsify_base_degree=2` or `3`, and `sparsify_reactivate_limit=-1` to use the built-in heuristic
425+
clamped to the compiled error count.
353426
## Help
354427

355428
* Do you have a feature request or want to report a bug? [Open an issue on

src/py/README.md

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ The `tesseract_decoder.tesseract` module provides the Tesseract decoder, which e
55

66
#### Class `tesseract.TesseractConfig`
77
This class holds the configuration parameters that control the behavior of the Tesseract decoder.
8-
* `TesseractConfig(dem: stim.DetectorErrorModel, det_beam: int = 5, beam_climbing: bool = False, no_revisit_dets: bool = True, verbose: bool = False, merge_errors: bool = True, pqlimit: int = 200000, det_orders: list[list[int]] = [], det_penalty: float = 0.0, create_visualization: bool = False)`
8+
* `TesseractConfig(dem: stim.DetectorErrorModel, det_beam: int = 5, beam_climbing: bool = False, no_revisit_dets: bool = True, verbose: bool = False, merge_errors: bool = True, pqlimit: int = 200000, det_orders: list[list[int]] = [], det_penalty: float = 0.0, create_visualization: bool = False, sparsify_errors: bool = False, sparsify_base_degree: int = -1, sparsify_max_degree: int = -1, sparsify_reactivate_limit: int = -1)`
99
* `__str__()`
1010

1111
Explanation of configuration arguments:
@@ -20,6 +20,13 @@ Explanation of configuration arguments:
2020
* `det_orders` - A list of lists of integers, where each inner list represents an ordering of the detectors. This is used for "ensemble reordering," an optimization that tries different detector orderings to improve the search's convergence. The default is an empty list, meaning a single, fixed ordering is used.
2121
* `det_penalty` - A floating-point value that adds a cost for each residual detection event. This encourages the decoder to prioritize paths that resolve more detection events, steering the search towards more complete solutions. The default value is `0.0`, meaning no penalty is applied.
2222
* `create_visualization` - A boolean flag that enables decoder visualization output when set to `True`. The default value is `False`.
23+
* `sparsify_errors` - Enables per-shot sparse error activation. When enabled, all errors up to `sparsify_base_degree` are always active, and selected higher-degree errors are reactivated per shot.
24+
* `sparsify_base_degree` - Required and positive when `sparsify_errors=True`. Errors with detector degree less than or equal to this value are always active.
25+
* `sparsify_max_degree` - Optional maximum degree for reactivated errors. Use `-1` for no maximum degree cap.
26+
* `sparsify_reactivate_limit` - Maximum number of optional high-degree errors to reactivate per shot. Use `-1` to apply the built-in heuristic, clamped to the number of errors in the compiled error model.
27+
28+
Module-level helper:
29+
* `suggest_sparsify_reactivate_limit(num_detectors, sparsify_base_degree)` - Returns the suggested reactivation limit for a detector count and base degree. The decoder applies this suggestion, clamped to the compiled error count, when `sparsify_reactivate_limit == -1`.
2330

2431
**Example Usage**:
2532

@@ -54,11 +61,26 @@ config2 = tesseract.TesseractConfig(
5461
det_penalty=0.1
5562
)
5663
print(f"Custom configuration detection beam: {config2.det_beam}")
57-
print(f"Custom configuration beam climbing: {config2.det_beam}")
58-
print(f"Custom configuration no-revisit detection events: {config2.det_beam}")
59-
print(f"Custom configuration pqlimit: {config2.det_beam}")
60-
print(f"Custom configuration verbose: {config2.det_beam}")
61-
print(f"Custom configuration detection penalty: {config2.det_beam}")
64+
print(f"Custom configuration beam climbing: {config2.beam_climbing}")
65+
print(f"Custom configuration no-revisit detection events: {config2.no_revisit_dets}")
66+
print(f"Custom configuration pqlimit: {config2.pqlimit}")
67+
print(f"Custom configuration verbose: {config2.verbose}")
68+
print(f"Custom configuration detection penalty: {config2.det_penalty}")
69+
70+
# Configuration with error sparsification
71+
config3 = tesseract.TesseractConfig(
72+
dem=dem,
73+
det_beam=20,
74+
beam_climbing=True,
75+
sparsify_errors=True,
76+
sparsify_base_degree=3,
77+
sparsify_reactivate_limit=-1,
78+
)
79+
decoder = config3.compile_decoder()
80+
print(
81+
"Resolved sparsify reactivation limit:",
82+
decoder.config.sparsify_reactivate_limit,
83+
)
6284
```
6385

6486
#### Class `tesseract.TesseractDecoder`
@@ -488,6 +510,20 @@ The Tesseract Python interface is compatible with the Sinter framework, which is
488510

489511
#### The TesseractSinterDecoder Object
490512
All Sinter examples rely on this utility function to provide the Sinter-compatible Tesseract decoder.
513+
The default decoder dictionary also includes sparsified variants:
514+
`tesseract-long-beam-sparsify-color-code-like`,
515+
`tesseract-long-beam-sparsify-surface-code-like`,
516+
`tesseract-short-beam-sparsify-color-code-like`, and
517+
`tesseract-short-beam-sparsify-surface-code-like`.
518+
519+
As a quick rule of thumb, use the non-sparsified decoders as the safest baseline. Use the
520+
`surface-code-like` variants for surface-code-like or mostly graphlike DEMs, and use the
521+
`color-code-like` variants for color-code,
522+
bivariate-bicycle-code, or other DEMs where a typical bulk data error activates about three
523+
detectors. Within either family, prefer the long-beam variants when accuracy matters more and the
524+
short-beam variants when runtime matters more. See the root README's
525+
[Performance Optimization](../../README.md#performance-optimization) section for the full
526+
sparsification details.
491527

492528
```python
493529
import sinter

src/py/generate_stubs.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def main():
8888

8989
# Build argv for pybind11-stubgen CLI.
9090
# --enum-class-locations maps enum names to their fully-qualified module path
91-
# so pybind11-stubgen can resolve default values like <DetOrder.DetBFS: 0>.
91+
# so pybind11-stubgen can resolve default values like <DetOrder.DetIndex: 1>.
9292
stubgen_argv = [
9393
"pybind11-stubgen",
9494
"tesseract_decoder",
@@ -139,4 +139,4 @@ def main():
139139

140140

141141
if __name__ == "__main__":
142-
main()
142+
main()

src/py/tesseract_sinter_compat_test.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def test_tesseract_sinter_obj_exists():
3333
decoder = TesseractSinterDecoder()
3434
assert hasattr(decoder, "compile_decoder_for_dem")
3535
assert hasattr(decoder, "decode_via_files")
36+
assert decoder.det_order_method == tesseract_decoder.utils.DetOrder.DetIndex
3637

3738

3839
@pytest.mark.parametrize("use_custom_config", [False, True])
@@ -687,5 +688,144 @@ def test_sinter_collect_different_dems():
687688
assert results.json_metadata["d"] == expected_distances[i]
688689

689690

691+
def test_tesseract_sinter_decoder_sparsify_attributes():
692+
decoder = TesseractSinterDecoder(
693+
sparsify_errors=True,
694+
sparsify_base_degree=2,
695+
sparsify_max_degree=4,
696+
sparsify_reactivate_limit=10,
697+
)
698+
assert decoder.sparsify_errors is True
699+
assert decoder.sparsify_base_degree == 2
700+
assert decoder.sparsify_max_degree == 4
701+
assert decoder.sparsify_reactivate_limit == 10
702+
703+
# Test equality
704+
decoder2 = TesseractSinterDecoder(
705+
sparsify_errors=True,
706+
sparsify_base_degree=2,
707+
sparsify_max_degree=4,
708+
sparsify_reactivate_limit=10,
709+
)
710+
assert decoder == decoder2
711+
712+
decoder3 = TesseractSinterDecoder(
713+
sparsify_errors=True,
714+
sparsify_base_degree=3, # different
715+
sparsify_max_degree=4,
716+
sparsify_reactivate_limit=10,
717+
)
718+
assert decoder != decoder3
719+
720+
# Test pickle
721+
import pickle
722+
723+
dumped = pickle.dumps(decoder)
724+
loaded = pickle.loads(dumped)
725+
assert decoder == loaded
726+
727+
728+
def test_tesseract_sinter_decoder_old_positional_constructor_order():
729+
decoder = TesseractSinterDecoder(
730+
20,
731+
True,
732+
True,
733+
False,
734+
True,
735+
1_000_000,
736+
0.0,
737+
False,
738+
21,
739+
tesseract_decoder.utils.DetOrder.DetIndex,
740+
2384753,
741+
)
742+
assert decoder.num_det_orders == 21
743+
assert decoder.det_order_method == tesseract_decoder.utils.DetOrder.DetIndex
744+
assert decoder.seed == 2384753
745+
assert decoder.sparsify_errors is False
746+
assert decoder.sparsify_base_degree == -1
747+
assert decoder.sparsify_max_degree == -1
748+
assert decoder.sparsify_reactivate_limit == -1
749+
750+
751+
def test_sinter_compile_sparsify_config_reaches_decoder():
752+
dem = stim.DetectorErrorModel("""
753+
error(0.1) D0
754+
detector(0, 0, 0) D0
755+
""")
756+
decoder = TesseractSinterDecoder(
757+
sparsify_errors=True,
758+
sparsify_base_degree=2,
759+
sparsify_reactivate_limit=-1,
760+
)
761+
compiled = decoder.compile_decoder_for_dem(dem=dem)
762+
assert compiled.decoder.config.sparsify_errors is True
763+
assert compiled.decoder.config.sparsify_base_degree == 2
764+
assert (
765+
compiled.decoder.config.sparsify_reactivate_limit
766+
== min(
767+
tesseract_decoder.tesseract.suggest_sparsify_reactivate_limit(
768+
dem.num_detectors,
769+
2,
770+
),
771+
dem.num_errors,
772+
)
773+
)
774+
775+
776+
def test_make_tesseract_sinter_decoders_dict_contains_sparsify():
777+
decoders = make_tesseract_sinter_decoders_dict()
778+
assert "tesseract-long-beam-sparsify-color-code-like" in decoders
779+
assert "tesseract-long-beam-sparsify-surface-code-like" in decoders
780+
assert "tesseract-short-beam-sparsify-color-code-like" in decoders
781+
assert "tesseract-short-beam-sparsify-surface-code-like" in decoders
782+
783+
d_long_color = decoders["tesseract-long-beam-sparsify-color-code-like"]
784+
assert d_long_color.sparsify_errors is True
785+
assert d_long_color.sparsify_base_degree == 3
786+
assert d_long_color.sparsify_max_degree == -1
787+
assert d_long_color.sparsify_reactivate_limit == -1
788+
assert d_long_color.det_beam == 20
789+
790+
d_short_surface = decoders["tesseract-short-beam-sparsify-surface-code-like"]
791+
assert d_short_surface.sparsify_errors is True
792+
assert d_short_surface.sparsify_base_degree == 2
793+
assert d_short_surface.sparsify_max_degree == -1
794+
assert d_short_surface.sparsify_reactivate_limit == -1
795+
assert d_short_surface.det_beam == 15
796+
797+
798+
@pytest.mark.parametrize(
799+
"decoder_name",
800+
[
801+
"tesseract-long-beam-sparsify-color-code-like",
802+
"tesseract-long-beam-sparsify-surface-code-like",
803+
"tesseract-short-beam-sparsify-color-code-like",
804+
"tesseract-short-beam-sparsify-surface-code-like",
805+
],
806+
)
807+
def test_sinter_decode_with_sparsify_decoders(decoder_name):
808+
# Test that the new decoders can actually run and decode a simple repetition code.
809+
circuit = stim.Circuit.generated(
810+
"repetition_code:memory",
811+
rounds=3,
812+
distance=3,
813+
after_clifford_depolarization=0.01,
814+
)
815+
816+
result = sample_decode(
817+
circuit_obj=circuit,
818+
circuit_path=None,
819+
dem_obj=circuit.detector_error_model(decompose_errors=True),
820+
dem_path=None,
821+
num_shots=100,
822+
decoder=decoder_name,
823+
custom_decoders=make_tesseract_sinter_decoders_dict(),
824+
)
825+
assert result.discards == 0
826+
assert result.shots == 100
827+
assert 0 <= result.errors <= 10
828+
829+
690830
if __name__ == "__main__":
691831
raise SystemExit(pytest.main([__file__]))

0 commit comments

Comments
 (0)