diff --git a/CMakeLists.txt b/CMakeLists.txt index 1a0a7ce..b7a4270 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,8 +19,8 @@ FetchContent_MakeAvailable(stim) # HiGHS FetchContent_Declare( highs - URL https://github.com/ERGO-Code/HiGHS/archive/refs/tags/v1.9.0.tar.gz - URL_HASH SHA256=dff575df08d88583c109702c7c5c75ff6e51611e6eacca8b5b3fdfba8ecc2cb4 + URL https://github.com/ERGO-Code/HiGHS/archive/refs/tags/v1.14.0.tar.gz + URL_HASH SHA256=05931e8dd8c8cac514da8297003c31a206a0004d542b7da500810b85c87c20b9 ) FetchContent_MakeAvailable(highs) diff --git a/README.md b/README.md index 77bf73e..7bbc692 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ Example with Advanced Options: --pqlimit 1000000 \ --no-revisit-dets \ --det-order-seed 232852747 \ - --det-order-index --num-det-orders 24 \ + --num-det-orders 24 \ --circuit circuit_file.stim \ --sample-seed 232856747 \ --sample-num-shots 10000 \ @@ -159,6 +159,35 @@ Here are some tips for improving performance: * *At most two errors per detector*: enable `--at-most-two-errors-per-detector` to improve performance. * *Priority Queue limit*: use `--pqlimit` to limit the size of the priority queue. +* *Error sparsification*: enable `--sparsify-errors` to always keep low-degree errors while + selectively reactivating high-degree errors per shot. This can improve runtime on DEMs with + many high-degree errors, at the cost of a tunable accuracy/speed tradeoff. + +Example with error sparsification: + +```bash +./tesseract \ + --circuit circuit_file.stim \ + --sample-num-shots 10000 \ + --beam 20 \ + --beam-climbing \ + --sparsify-errors \ + --sparsify-base-degree 3 \ + --print-stats +``` + +`--sparsify-base-degree K` is required when `--sparsify-errors` is enabled. Errors touching at most +`K` detectors are always active. Errors above `K` are optional and are ranked per shot by overlap +with the fired detectors. In the surface code (or other 'mostly graphlike' codes) try K = 2. In the +color code or bivariate bicycle codes, try K = 3. In general, it is recommended to set K to the number +of activated detectors created by a single data qubit error in the bulk, restricting to X or Z errors +only for CSS codes. + +`--sparsify-reactivate-limit M` caps the number of optional high-degree errors reactivated per +shot. If omitted, Tesseract uses `round((4.5^(K - 2) / 3) * num_detectors)`. + +`--sparsify-max-degree D` optionally excludes optional errors above degree `D`. If omitted, optional +errors are not capped by degree. ### Output Formats @@ -197,8 +226,21 @@ dem = stim.DetectorErrorModel(""" # 2. Create the decoder configuration config = tesseract.TesseractConfig(dem=dem, det_beam=50) +# To enable sparse activation for high-degree DEMs: +config = tesseract.TesseractConfig( + dem=dem, + det_beam=50, + sparsify_errors=True, + sparsify_base_degree=3, + sparsify_reactivate_limit=-1, # Use the built-in heuristic, clamped to error count. +) + # 3. Create a decoder instance decoder = config.compile_decoder() +print( + "Resolved sparsify reactivation limit:", + decoder.config.sparsify_reactivate_limit, +) # 4. Simulate detector outcomes syndrome = np.array([0, 1, 1], dtype=bool) @@ -235,7 +277,12 @@ if __name__ == "__main__": 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'] + decoders = [ + 'tesseract', + 'tesseract-long-beam', + 'tesseract-short-beam', + 'tesseract-long-beam-sparsify3', + ] 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') @@ -248,6 +295,9 @@ if __name__ == "__main__": num_det_orders=5, det_order_method=tesseract_decoder.utils.DetOrder.DetIndex, seed=2384753, + sparsify_errors=True, + sparsify_base_degree=3, + sparsify_reactivate_limit=-1, ) for distance in [3, 5, 7]: @@ -285,13 +335,16 @@ should get something like: 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, 14, 0, 4.12,tesseract-long-beam-sparsify3,14fa5f9f08381d760f6c1f59805b75f2c70cfb83e50d9f1f40d92820a20eeb13,"{""d"":5,""decoder"":""tesseract-long-beam-sparsify3""}", 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, 35, 0, 0.681,tesseract-long-beam-sparsify3,f41bdb1bde3f5cf4893a9a9e33fc7d4c47d742f22b13dfec9195347e780119bc,"{""d"":3,""decoder"":""tesseract-long-beam-sparsify3""}", 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, 4, 0, 14.8,tesseract-long-beam-sparsify3,80868acc6e43c62cb73b242b66ae27d3ea08fe970ea879db5a8425c2454fc8a1,"{""d"":7,""decoder"":""tesseract-long-beam-sparsify3""}", 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""}", ``` @@ -348,8 +401,25 @@ tesseract_config = tesseract.TesseractConfig( no_revisit_dets=True, ) ``` -For `det_order`, you can use two other options of `DetIndex` and `DetCoordinate` as well. +`DetIndex` is the default detector ordering. You can also pass `DetBFS` or `DetCoordinate` +explicitly. These values balance decoding speed and accuracy across the benchmarks reported in the paper and can be adjusted for specific use cases. + +The Sinter decoder dictionary also provides sparsified variants: +`tesseract-long-beam-sparsify3`, `tesseract-long-beam-sparsify2`, +`tesseract-short-beam-sparsify3`, and `tesseract-short-beam-sparsify2`. The suffix indicates the +sparsification base degree. + +As a quick rule of thumb, use the non-sparsified decoders as the safest baseline. Use `sparsify2` +for surface-code-like or mostly graphlike DEMs, and use `sparsify3` for color-code, +bivariate-bicycle-code, or other DEMs where a typical bulk data error activates about three +detectors. Within either family, prefer the long-beam variants when accuracy matters more and the +short-beam variants when runtime matters more. See the +[Performance Optimization](#performance-optimization) section for the full sparsification details. + +Equivalent Python configs can enable sparsification with `sparsify_errors=True`, +`sparsify_base_degree=2` or `3`, and `sparsify_reactivate_limit=-1` to use the built-in heuristic +clamped to the compiled error count. ## Help * Do you have a feature request or want to report a bug? [Open an issue on diff --git a/src/py/README.md b/src/py/README.md index 32a9a36..5d9c8be 100644 --- a/src/py/README.md +++ b/src/py/README.md @@ -5,7 +5,7 @@ The `tesseract_decoder.tesseract` module provides the Tesseract decoder, which e #### Class `tesseract.TesseractConfig` This class holds the configuration parameters that control the behavior of the Tesseract decoder. -* `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)` +* `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)` * `__str__()` Explanation of configuration arguments: @@ -20,6 +20,13 @@ Explanation of configuration arguments: * `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. * `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. * `create_visualization` - A boolean flag that enables decoder visualization output when set to `True`. The default value is `False`. +* `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. +* `sparsify_base_degree` - Required when `sparsify_errors=True`. Errors with detector degree less than or equal to this value are always active. +* `sparsify_max_degree` - Optional maximum degree for reactivated errors. Use `-1` for no maximum degree cap. +* `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. + +Module-level helper: +* `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`. **Example Usage**: @@ -59,6 +66,21 @@ print(f"Custom configuration no-revisit detection events: {config2.det_beam}") print(f"Custom configuration pqlimit: {config2.det_beam}") print(f"Custom configuration verbose: {config2.det_beam}") print(f"Custom configuration detection penalty: {config2.det_beam}") + +# Configuration with error sparsification +config3 = tesseract.TesseractConfig( + dem=dem, + det_beam=20, + beam_climbing=True, + sparsify_errors=True, + sparsify_base_degree=3, + sparsify_reactivate_limit=-1, +) +decoder = config3.compile_decoder() +print( + "Resolved sparsify reactivation limit:", + decoder.config.sparsify_reactivate_limit, +) ``` #### Class `tesseract.TesseractDecoder` @@ -488,6 +510,17 @@ The Tesseract Python interface is compatible with the Sinter framework, which is #### The TesseractSinterDecoder Object All Sinter examples rely on this utility function to provide the Sinter-compatible Tesseract decoder. +The default decoder dictionary also includes sparsified variants: +`tesseract-long-beam-sparsify3`, `tesseract-long-beam-sparsify2`, +`tesseract-short-beam-sparsify3`, and `tesseract-short-beam-sparsify2`. + +As a quick rule of thumb, use the non-sparsified decoders as the safest baseline. Use `sparsify2` +for surface-code-like or mostly graphlike DEMs, and use `sparsify3` for color-code, +bivariate-bicycle-code, or other DEMs where a typical bulk data error activates about three +detectors. Within either family, prefer the long-beam variants when accuracy matters more and the +short-beam variants when runtime matters more. See the root README's +[Performance Optimization](../../README.md#performance-optimization) section for the full +sparsification details. ```python import sinter diff --git a/src/py/generate_stubs.py b/src/py/generate_stubs.py index 4bec09b..251796b 100644 --- a/src/py/generate_stubs.py +++ b/src/py/generate_stubs.py @@ -88,7 +88,7 @@ def main(): # Build argv for pybind11-stubgen CLI. # --enum-class-locations maps enum names to their fully-qualified module path - # so pybind11-stubgen can resolve default values like . + # so pybind11-stubgen can resolve default values like . stubgen_argv = [ "pybind11-stubgen", "tesseract_decoder", @@ -139,4 +139,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/src/py/tesseract_sinter_compat_test.py b/src/py/tesseract_sinter_compat_test.py index 8a4e15a..2f61a01 100644 --- a/src/py/tesseract_sinter_compat_test.py +++ b/src/py/tesseract_sinter_compat_test.py @@ -33,6 +33,7 @@ def test_tesseract_sinter_obj_exists(): decoder = TesseractSinterDecoder() assert hasattr(decoder, "compile_decoder_for_dem") assert hasattr(decoder, "decode_via_files") + assert decoder.det_order_method == tesseract_decoder.utils.DetOrder.DetIndex @pytest.mark.parametrize("use_custom_config", [False, True]) @@ -687,5 +688,144 @@ def test_sinter_collect_different_dems(): assert results.json_metadata["d"] == expected_distances[i] +def test_tesseract_sinter_decoder_sparsify_attributes(): + decoder = TesseractSinterDecoder( + sparsify_errors=True, + sparsify_base_degree=2, + sparsify_max_degree=4, + sparsify_reactivate_limit=10, + ) + assert decoder.sparsify_errors is True + assert decoder.sparsify_base_degree == 2 + assert decoder.sparsify_max_degree == 4 + assert decoder.sparsify_reactivate_limit == 10 + + # Test equality + decoder2 = TesseractSinterDecoder( + sparsify_errors=True, + sparsify_base_degree=2, + sparsify_max_degree=4, + sparsify_reactivate_limit=10, + ) + assert decoder == decoder2 + + decoder3 = TesseractSinterDecoder( + sparsify_errors=True, + sparsify_base_degree=3, # different + sparsify_max_degree=4, + sparsify_reactivate_limit=10, + ) + assert decoder != decoder3 + + # Test pickle + import pickle + + dumped = pickle.dumps(decoder) + loaded = pickle.loads(dumped) + assert decoder == loaded + + +def test_tesseract_sinter_decoder_old_positional_constructor_order(): + decoder = TesseractSinterDecoder( + 20, + True, + True, + False, + True, + 1_000_000, + 0.0, + False, + 21, + tesseract_decoder.utils.DetOrder.DetIndex, + 2384753, + ) + assert decoder.num_det_orders == 21 + assert decoder.det_order_method == tesseract_decoder.utils.DetOrder.DetIndex + assert decoder.seed == 2384753 + assert decoder.sparsify_errors is False + assert decoder.sparsify_base_degree == -1 + assert decoder.sparsify_max_degree == -1 + assert decoder.sparsify_reactivate_limit == -1 + + +def test_sinter_compile_sparsify_config_reaches_decoder(): + dem = stim.DetectorErrorModel(""" + error(0.1) D0 + detector(0, 0, 0) D0 + """) + decoder = TesseractSinterDecoder( + sparsify_errors=True, + sparsify_base_degree=2, + sparsify_reactivate_limit=-1, + ) + compiled = decoder.compile_decoder_for_dem(dem=dem) + assert compiled.decoder.config.sparsify_errors is True + assert compiled.decoder.config.sparsify_base_degree == 2 + assert ( + compiled.decoder.config.sparsify_reactivate_limit + == min( + tesseract_decoder.tesseract.suggest_sparsify_reactivate_limit( + dem.num_detectors, + 2, + ), + dem.num_errors, + ) + ) + + +def test_make_tesseract_sinter_decoders_dict_contains_sparsify(): + decoders = make_tesseract_sinter_decoders_dict() + assert "tesseract-long-beam-sparsify3" in decoders + assert "tesseract-long-beam-sparsify2" in decoders + assert "tesseract-short-beam-sparsify3" in decoders + assert "tesseract-short-beam-sparsify2" in decoders + + d_long3 = decoders["tesseract-long-beam-sparsify3"] + assert d_long3.sparsify_errors is True + assert d_long3.sparsify_base_degree == 3 + assert d_long3.sparsify_max_degree == -1 + assert d_long3.sparsify_reactivate_limit == -1 + assert d_long3.det_beam == 20 + + d_short2 = decoders["tesseract-short-beam-sparsify2"] + assert d_short2.sparsify_errors is True + assert d_short2.sparsify_base_degree == 2 + assert d_short2.sparsify_max_degree == -1 + assert d_short2.sparsify_reactivate_limit == -1 + assert d_short2.det_beam == 15 + + +@pytest.mark.parametrize( + "decoder_name", + [ + "tesseract-long-beam-sparsify3", + "tesseract-long-beam-sparsify2", + "tesseract-short-beam-sparsify3", + "tesseract-short-beam-sparsify2", + ], +) +def test_sinter_decode_with_sparsify_decoders(decoder_name): + # Test that the new decoders can actually run and decode a simple repetition code. + circuit = stim.Circuit.generated( + "repetition_code:memory", + rounds=3, + distance=3, + after_clifford_depolarization=0.01, + ) + + result = sample_decode( + circuit_obj=circuit, + circuit_path=None, + dem_obj=circuit.detector_error_model(decompose_errors=True), + dem_path=None, + num_shots=100, + decoder=decoder_name, + custom_decoders=make_tesseract_sinter_decoders_dict(), + ) + assert result.discards == 0 + assert result.shots == 100 + assert 0 <= result.errors <= 10 + + if __name__ == "__main__": raise SystemExit(pytest.main([__file__])) diff --git a/src/py/tesseract_test.py b/src/py/tesseract_test.py index bb62bea..d9955c8 100644 --- a/src/py/tesseract_test.py +++ b/src/py/tesseract_test.py @@ -47,6 +47,12 @@ def test_create_tesseract_config(): assert config.det_penalty == 0 assert config.create_visualization is False assert len(config.det_orders) == 20 + assert config.det_orders == tesseract_decoder.utils.build_det_orders( + _DETECTOR_ERROR_MODEL, + 20, + tesseract_decoder.utils.DetOrder.DetIndex, + 2384753, + ) def test_create_tesseract_config_with_dem(): @@ -286,5 +292,149 @@ def test_test_simplex_decode_batch_with_mismatched_syndrome_size(): ) +def test_create_tesseract_config_sparsify_defaults(): + config = tesseract_decoder.tesseract.TesseractConfig() + assert config.sparsify_errors is False + assert config.sparsify_base_degree == -1 + assert config.sparsify_max_degree == -1 + assert config.sparsify_reactivate_limit == -1 + + +def test_create_tesseract_config_sparsify_custom(): + config = tesseract_decoder.tesseract.TesseractConfig( + sparsify_errors=True, + sparsify_base_degree=2, + sparsify_max_degree=4, + sparsify_reactivate_limit=10, + ) + assert config.sparsify_errors is True + assert config.sparsify_base_degree == 2 + assert config.sparsify_max_degree == 4 + assert config.sparsify_reactivate_limit == 10 + + +def test_suggest_sparsify_reactivate_limit(): + # Heuristic formula: round((4.5^(k-2) / 3) * num_detectors) + assert tesseract_decoder.tesseract.suggest_sparsify_reactivate_limit(2, 2) == 1 + assert tesseract_decoder.tesseract.suggest_sparsify_reactivate_limit(2, 3) == 3 + assert tesseract_decoder.tesseract.suggest_sparsify_reactivate_limit(0, 2) == 0 + assert ( + tesseract_decoder.tesseract.suggest_sparsify_reactivate_limit(1, 10_000) + == 2_147_483_647 + ) + with pytest.raises(ValueError, match="sparsify_base_degree must be >= 0"): + tesseract_decoder.tesseract.suggest_sparsify_reactivate_limit(2, -1) + + +@pytest.mark.parametrize( + "kwargs, message", + [ + ( + {"sparsify_reactivate_limit": -2}, + "sparsify_reactivate_limit must be >= -1", + ), + ( + {"sparsify_max_degree": -2}, + "sparsify_max_degree must be >= -1", + ), + ], +) +def test_sparsify_negative_sentinels_rejected(kwargs, message): + config = tesseract_decoder.tesseract.TesseractConfig( + _DETECTOR_ERROR_MODEL, + sparsify_errors=True, + sparsify_base_degree=2, + **kwargs, + ) + with pytest.raises(ValueError, match=message): + config.compile_decoder() + + +def test_compile_decoder_resolves_auto_sparsify_reactivate_limit(): + config = tesseract_decoder.tesseract.TesseractConfig( + _DETECTOR_ERROR_MODEL, + sparsify_errors=True, + sparsify_base_degree=2, + sparsify_reactivate_limit=-1, + ) + decoder = config.compile_decoder() + assert ( + decoder.config.sparsify_reactivate_limit + == min( + tesseract_decoder.tesseract.suggest_sparsify_reactivate_limit( + _DETECTOR_ERROR_MODEL.num_detectors, + 2, + ), + _DETECTOR_ERROR_MODEL.num_errors, + ) + ) + + +def test_compile_decoder_preserves_explicit_sparsify_reactivate_limit(): + config = tesseract_decoder.tesseract.TesseractConfig( + _DETECTOR_ERROR_MODEL, + sparsify_errors=True, + sparsify_base_degree=2, + sparsify_reactivate_limit=10, + ) + decoder = config.compile_decoder() + assert decoder.config.sparsify_reactivate_limit == 10 + + +def test_python_sparsify_changes_predicted_error_set(): + dem = stim.DetectorErrorModel(""" + error(0.1) D0 + error(0.1) D1 + error(0.1) D2 + error(0.1) D3 + error(0.01) D0 D1 D2 D3 + """) + syndrome = np.array([1, 1, 1, 1], dtype=bool) + + dense = tesseract_decoder.tesseract.TesseractConfig( + dem, + merge_errors=False, + ).compile_decoder() + dense.decode_to_errors(syndrome) + assert list(dense.predicted_errors_buffer) == [4] + + sparse0 = tesseract_decoder.tesseract.TesseractConfig( + dem, + merge_errors=False, + sparsify_errors=True, + sparsify_base_degree=2, + sparsify_max_degree=4, + sparsify_reactivate_limit=0, + ).compile_decoder() + sparse0.decode_to_errors(syndrome) + assert sorted(sparse0.predicted_errors_buffer) == [0, 1, 2, 3] + + sparse1 = tesseract_decoder.tesseract.TesseractConfig( + dem, + merge_errors=False, + sparsify_errors=True, + sparsify_base_degree=2, + sparsify_max_degree=4, + sparsify_reactivate_limit=1, + ).compile_decoder() + sparse1.decode_to_errors(syndrome) + assert list(sparse1.predicted_errors_buffer) == [4] + + +def test_decoder_compilation_validation(): + # sparsify_base_degree < 0 throws + config = tesseract_decoder.tesseract.TesseractConfig( + _DETECTOR_ERROR_MODEL, sparsify_errors=True, sparsify_base_degree=-1 + ) + with pytest.raises(ValueError, match="sparsify_base_degree must be >= 0"): + config.compile_decoder() + + # sparsify_max_degree < sparsify_base_degree throws + config.sparsify_base_degree = 3 + config.sparsify_max_degree = 2 + with pytest.raises(ValueError, match="sparsify_max_degree must be >= sparsify_base_degree"): + config.compile_decoder() + + if __name__ == "__main__": raise SystemExit(pytest.main([__file__])) diff --git a/src/py/utils_test.py b/src/py/utils_test.py index ed7d642..6e953dd 100644 --- a/src/py/utils_test.py +++ b/src/py/utils_test.py @@ -45,9 +45,21 @@ def test_build_detector_graph(): ] +def test_build_det_orders_default_index(): + res = tesseract_decoder.utils.build_det_orders( + _DETECTOR_ERROR_MODEL_10, num_det_orders=1, seed=0 + ) + expected_asc = list(range(10)) + expected_desc = list(range(9, -1, -1)) + assert res == [expected_asc] or res == [expected_desc] + + def test_build_det_orders_bfs(): assert tesseract_decoder.utils.build_det_orders( - _DETECTOR_ERROR_MODEL, num_det_orders=1, seed=0 + _DETECTOR_ERROR_MODEL, + num_det_orders=1, + method=tesseract_decoder.utils.DetOrder.DetBFS, + seed=0, ) == [[0, 1]] diff --git a/src/tesseract.cc b/src/tesseract.cc index 91651e0..0820ec1 100644 --- a/src/tesseract.cc +++ b/src/tesseract.cc @@ -17,6 +17,7 @@ #include #include // For boost::hash_range #include +#include #include #include // For std::hash (though not strictly necessary here, but good practice) #include @@ -71,6 +72,31 @@ std::string TesseractConfig::str() { return ss.str(); } +int suggest_sparsify_reactivate_limit(size_t num_detectors, int sparsify_base_degree) { + if (sparsify_base_degree < 0) { + throw std::invalid_argument("sparsify_base_degree must be >= 0."); + } + if (num_detectors == 0) { + return 0; + } + double exponent = static_cast(sparsify_base_degree) - 2.0; + double max_result = static_cast(std::numeric_limits::max()); + double log_result = + exponent * std::log(4.5) - std::log(3.0) + std::log(static_cast(num_detectors)); + if (log_result >= std::log(max_result)) { + return std::numeric_limits::max(); + } + double result = (std::pow(4.5, exponent) / 3.0) * static_cast(num_detectors); + if (!std::isfinite(result)) { + return std::numeric_limits::max(); + } + double rounded = std::round(result); + if (rounded >= max_result) { + return std::numeric_limits::max(); + } + return static_cast(rounded); +} + std::string Node::str() { std::stringstream ss; auto& self = *this; @@ -217,6 +243,29 @@ void TesseractDecoder::initialize_structures(size_t num_detectors) { } if (config.sparsify_errors) { + if (config.sparsify_base_degree < 0) { + throw std::invalid_argument( + "sparsify_base_degree must be >= 0 when sparsify_errors is enabled."); + } + if (config.sparsify_max_degree < -1) { + throw std::invalid_argument("sparsify_max_degree must be >= -1."); + } + if (config.sparsify_reactivate_limit < -1) { + throw std::invalid_argument("sparsify_reactivate_limit must be >= -1."); + } + if (config.sparsify_max_degree >= 0 && + config.sparsify_max_degree < config.sparsify_base_degree) { + throw std::invalid_argument("sparsify_max_degree must be >= sparsify_base_degree."); + } + + if (config.sparsify_reactivate_limit == -1) { + int suggested_reactivate_limit = suggest_sparsify_reactivate_limit( + config.dem.count_detectors(), config.sparsify_base_degree); + int error_count_limit = static_cast( + std::min(num_errors, static_cast(std::numeric_limits::max()))); + config.sparsify_reactivate_limit = std::min(suggested_reactivate_limit, error_count_limit); + } + sparsify_mandatory_errors.clear(); sparsify_optional_errors.clear(); for (size_t ei = 0; ei < num_errors; ++ei) { @@ -326,7 +375,9 @@ void TesseractDecoder::decode_to_errors_with_graph( error_chain_arena.clear(); // Can technically be larger than pqlimit, but we need an initial guess on how many nodes we // will process from the queue. - error_chain_arena.reserve(config.pqlimit); + if (config.pqlimit != std::numeric_limits::max()) { + error_chain_arena.reserve(config.pqlimit); + } std::priority_queue, std::greater> pq; std::unordered_map>> visited_detectors; diff --git a/src/tesseract.h b/src/tesseract.h index 37b5531..97b88eb 100644 --- a/src/tesseract.h +++ b/src/tesseract.h @@ -32,6 +32,8 @@ constexpr size_t INF_DET_BEAM = std::numeric_limits::max(); constexpr int DEFAULT_DET_BEAM = 5; constexpr size_t DEFAULT_PQLIMIT = 200000; +int suggest_sparsify_reactivate_limit(size_t num_detectors, int sparsify_base_degree); + struct TesseractConfig { stim::DetectorErrorModel dem; int det_beam = DEFAULT_DET_BEAM; diff --git a/src/tesseract.pybind.h b/src/tesseract.pybind.h index 3bdf477..cebe512 100644 --- a/src/tesseract.pybind.h +++ b/src/tesseract.pybind.h @@ -38,13 +38,17 @@ TesseractConfig tesseract_config_maker_no_dem( bool verbose = false, bool merge_errors = true, size_t pqlimit = std::numeric_limits::max(), std::vector> det_orders = std::vector>(), - double det_penalty = 0.0, bool create_visualization = false) { + double det_penalty = 0.0, bool create_visualization = false, bool sparsify_errors = false, + int sparsify_base_degree = -1, int sparsify_max_degree = -1, + int sparsify_reactivate_limit = -1) { stim::DetectorErrorModel empty_dem; if (det_orders.empty()) { - det_orders = build_det_orders(empty_dem, 20, DetOrder::DetBFS, 2384753); + det_orders = build_det_orders(empty_dem, 20, DetOrder::DetIndex, 2384753); } return TesseractConfig({empty_dem, det_beam, beam_climbing, no_revisit_dets, verbose, - merge_errors, pqlimit, det_orders, det_penalty, create_visualization}); + merge_errors, pqlimit, det_orders, det_penalty, create_visualization, + sparsify_errors, sparsify_base_degree, sparsify_max_degree, + sparsify_reactivate_limit}); } TesseractConfig tesseract_config_maker( @@ -52,13 +56,17 @@ TesseractConfig tesseract_config_maker( bool no_revisit_dets = false, bool verbose = false, bool merge_errors = true, size_t pqlimit = std::numeric_limits::max(), std::vector> det_orders = std::vector>(), - double det_penalty = 0.0, bool create_visualization = false) { + double det_penalty = 0.0, bool create_visualization = false, bool sparsify_errors = false, + int sparsify_base_degree = -1, int sparsify_max_degree = -1, + int sparsify_reactivate_limit = -1) { stim::DetectorErrorModel input_dem = parse_py_object(dem); if (det_orders.empty()) { - det_orders = build_det_orders(input_dem, 20, DetOrder::DetBFS, 2384753); + det_orders = build_det_orders(input_dem, 20, DetOrder::DetIndex, 2384753); } return TesseractConfig({input_dem, det_beam, beam_climbing, no_revisit_dets, verbose, - merge_errors, pqlimit, det_orders, det_penalty, create_visualization}); + merge_errors, pqlimit, det_orders, det_penalty, create_visualization, + sparsify_errors, sparsify_base_degree, sparsify_max_degree, + sparsify_reactivate_limit}); } }; // namespace @@ -67,6 +75,9 @@ void add_tesseract_module(py::module& root) { m.attr("INF_DET_BEAM") = INF_DET_BEAM; m.doc() = "A sentinel value indicating an infinite beam size for the decoder."; + m.def("suggest_sparsify_reactivate_limit", &suggest_sparsify_reactivate_limit, + py::arg("num_detectors"), py::arg("sparsify_base_degree"), + "Returns the suggested number of optional high-degree errors to reactivate per shot."); py::class_(m, "TesseractConfig", R"pbdoc( Configuration object for the `TesseractDecoder`. @@ -82,7 +93,9 @@ void add_tesseract_module(py::module& root) { py::arg("beam_climbing") = false, py::arg("no_revisit_dets") = true, py::arg("verbose") = false, py::arg("merge_errors") = true, py::arg("pqlimit") = 200000, py::arg("det_orders") = std::vector>(), py::arg("det_penalty") = 0.0, - py::arg("create_visualization") = false, + py::arg("create_visualization") = false, py::arg("sparsify_errors") = false, + py::arg("sparsify_base_degree") = -1, py::arg("sparsify_max_degree") = -1, + py::arg("sparsify_reactivate_limit") = -1, R"pbdoc( The constructor for the `TesseractConfig` class without a `dem` argument. This creates an empty `DetectorErrorModel` by default. @@ -109,12 +122,22 @@ void add_tesseract_module(py::module& root) { A penalty value added to the cost of each detector visited. create_visualization: bool, defualt=False Whether to record the information needed to create a visualization or not. + sparsify_errors: bool, default=False + If True, enables per-shot sparse error activation. + sparsify_base_degree: int, default=-1 + Maximum detector degree for mandatory errors. + sparsify_max_degree: int, default=-1 + Maximum detector degree for optional errors. + sparsify_reactivate_limit: int, default=-1 + Maximum number of optional errors to reactivate per shot. Use -1 for heuristic default. )pbdoc") .def(py::init(&tesseract_config_maker), py::arg("dem"), py::arg("det_beam") = 5, py::arg("beam_climbing") = false, py::arg("no_revisit_dets") = true, py::arg("verbose") = false, py::arg("merge_errors") = true, py::arg("pqlimit") = 200000, py::arg("det_orders") = std::vector>(), py::arg("det_penalty") = 0.0, - py::arg("create_visualization") = false, + py::arg("create_visualization") = false, py::arg("sparsify_errors") = false, + py::arg("sparsify_base_degree") = -1, py::arg("sparsify_max_degree") = -1, + py::arg("sparsify_reactivate_limit") = -1, R"pbdoc( The constructor for the `TesseractConfig` class. @@ -142,6 +165,14 @@ void add_tesseract_module(py::module& root) { A penalty value added to the cost of each detector visited. create_visualization: bool, defualt=False Whether to record the information needed to create a visualization or not. + sparsify_errors: bool, default=False + If True, enables per-shot sparse error activation. + sparsify_base_degree: int, default=-1 + Maximum detector degree for mandatory errors. + sparsify_max_degree: int, default=-1 + Maximum detector degree for optional errors. + sparsify_reactivate_limit: int, default=-1 + Maximum number of optional errors to reactivate per shot. Use -1 for heuristic default. )pbdoc") .def_property("dem", &dem_getter, &dem_setter, "The `stim.DetectorErrorModel` that defines the error channels and detectors.") @@ -164,6 +195,15 @@ void add_tesseract_module(py::module& root) { "The penalty cost added for each detector.") .def_readwrite("create_visualization", &TesseractConfig::create_visualization, "If True, records necessary information to create visualization.") + .def_readwrite("sparsify_errors", &TesseractConfig::sparsify_errors, + "If True, enables per-shot sparse error activation.") + .def_readwrite("sparsify_base_degree", &TesseractConfig::sparsify_base_degree, + "Maximum detector degree for mandatory errors.") + .def_readwrite("sparsify_max_degree", &TesseractConfig::sparsify_max_degree, + "Maximum detector degree for optional errors.") + .def_readwrite("sparsify_reactivate_limit", &TesseractConfig::sparsify_reactivate_limit, + "Maximum number of optional errors to reactivate per shot. Use -1 for " + "heuristic default.") .def("__str__", &TesseractConfig::str) .def("compile_decoder", &_compile_tesseract_decoder_helper, py::return_value_policy::take_ownership, diff --git a/src/tesseract.test.cc b/src/tesseract.test.cc index 233416f..b6db78f 100644 --- a/src/tesseract.test.cc +++ b/src/tesseract.test.cc @@ -419,6 +419,59 @@ TEST(TesseractDetcostTest, ComparesRatiosNotRawCosts) { EXPECT_NEAR(got, expected, 1e-12); } +TEST(TesseractSparsifyTest, SuggestReactivateLimit) { + EXPECT_EQ(suggest_sparsify_reactivate_limit(2, 2), 1); + EXPECT_EQ(suggest_sparsify_reactivate_limit(2, 3), 3); + EXPECT_EQ(suggest_sparsify_reactivate_limit(0, 2), 0); + EXPECT_EQ(suggest_sparsify_reactivate_limit(1, std::numeric_limits::max()), + std::numeric_limits::max()); + EXPECT_THROW(suggest_sparsify_reactivate_limit(2, -1), std::invalid_argument); +} + +TEST(TesseractSparsifyTest, AutoReactivateLimitClampedToErrorCount) { + stim::DetectorErrorModel dem = stim::DetectorErrorModel(R"DEM( + error(0.1) D0 + detector(0, 0, 0) D0 + detector(1, 0, 0) D1 + detector(2, 0, 0) D2 + detector(3, 0, 0) D3 + detector(4, 0, 0) D4 + detector(5, 0, 0) D5 + detector(6, 0, 0) D6 + detector(7, 0, 0) D7 + detector(8, 0, 0) D8 + detector(9, 0, 0) D9 + )DEM"); + + TesseractConfig cfg; + cfg.dem = dem; + cfg.merge_errors = false; + cfg.sparsify_errors = true; + cfg.sparsify_base_degree = 3; + cfg.sparsify_reactivate_limit = -1; + TesseractDecoder dec(cfg); + + EXPECT_EQ(dec.config.sparsify_reactivate_limit, 1); +} + +TEST(tesseract, InfinitePqlimitDoesNotReserveMaxVector) { + stim::DetectorErrorModel dem = stim::DetectorErrorModel(R"DEM( + error(0.1) D0 + error(0.1) D1 + error(0.2) D0 D1 L0 + )DEM"); + + TesseractConfig cfg; + cfg.dem = dem; + cfg.merge_errors = false; + cfg.pqlimit = std::numeric_limits::max(); + TesseractDecoder dec(cfg); + + EXPECT_NO_THROW(dec.decode_to_errors({0, 1})); + std::vector expected = {2}; + EXPECT_EQ(dec.predicted_errors_buffer, expected); +} + TEST(TesseractSparsifyTest, HighDegreeErrorRemoved) { stim::DetectorErrorModel dem = stim::DetectorErrorModel(R"DEM( error(0.1) D0 diff --git a/src/tesseract_main.cc b/src/tesseract_main.cc index bcdf74e..1d390d3 100644 --- a/src/tesseract_main.cc +++ b/src/tesseract_main.cc @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -225,8 +226,10 @@ struct Args { std::cout << ")" << std::endl; } } - DetOrder order = DetOrder::DetBFS; - if (det_order_index) { + DetOrder order = DetOrder::DetIndex; + if (det_order_bfs) { + order = DetOrder::DetBFS; + } else if (det_order_index) { order = DetOrder::DetIndex; } else if (det_order_coordinate) { order = DetOrder::DetCoordinate; @@ -334,15 +337,6 @@ struct Args { config.sparsify_errors = sparsify_errors; config.sparsify_base_degree = sparsify_base_degree; config.sparsify_max_degree = sparsify_max_degree; - - // Apply heuristic estimate for number of errors if sparsify_errors is enabled but no limit was - // provided - if (sparsify_errors && sparsify_reactivate_limit < 0) { - double k = sparsify_base_degree; - double num_detectors = config.dem.count_detectors(); - sparsify_reactivate_limit = - static_cast(std::round((std::pow(4.5, k - 2.0) / 3.0) * num_detectors)); - } config.sparsify_reactivate_limit = sparsify_reactivate_limit; } }; @@ -362,11 +356,13 @@ int main(int argc, char* argv[]) { .default_value(size_t(1)) .store_into(args.num_det_orders); program.add_argument("--det-order-bfs") - .help("Use BFS-based detector ordering (default if no method specified)") + .help("Use BFS-based detector ordering") .flag() .store_into(args.det_order_bfs); program.add_argument("--det-order-index") - .help("Randomly choose increasing or decreasing detector index order") + .help( + "Randomly choose increasing or decreasing detector index order " + "(default if no method specified)") .flag() .store_into(args.det_order_index); program.add_argument("--det-order-coordinate") @@ -633,30 +629,47 @@ int main(int argc, char* argv[]) { out << est_dem << '\n'; } + int effective_sparsify_reactivate_limit = config.sparsify_reactivate_limit; + for (const auto& decoder : decoders) { + if (decoder) { + effective_sparsify_reactivate_limit = decoder->config.sparsify_reactivate_limit; + break; + } + } + if (config.sparsify_errors && effective_sparsify_reactivate_limit == -1) { + effective_sparsify_reactivate_limit = suggest_sparsify_reactivate_limit( + config.dem.count_detectors(), config.sparsify_base_degree); + effective_sparsify_reactivate_limit = std::min( + effective_sparsify_reactivate_limit, + static_cast(std::min( + config.dem.count_errors(), static_cast(std::numeric_limits::max())))); + } + bool print_final_stats = true; if (!args.stats_out_fname.empty()) { - nlohmann::json stats_json = {{"circuit_path", args.circuit_path}, - {"dem_path", args.dem_path}, - {"max_errors", args.max_errors}, - {"sample_seed", args.sample_seed}, - - {"det_beam", args.det_beam}, - {"det_penalty", args.det_penalty}, - {"beam_climbing", args.beam_climbing}, - {"no_revisit_dets", args.no_revisit_dets}, - {"pqlimit", args.pqlimit}, - {"num_det_orders", args.num_det_orders}, - {"det_order_seed", args.det_order_seed}, - {"total_time_seconds", total_time_seconds}, - {"num_errors", num_errors}, - {"num_low_confidence", num_low_confidence}, - {"num_shots", shot}, - {"num_threads", args.num_threads}, - {"sample_num_shots", args.sample_num_shots}, - {"sparsify_errors", args.sparsify_errors}, - {"sparsify_base_degree", args.sparsify_base_degree}, - {"sparsify_max_degree", args.sparsify_max_degree}, - {"sparsify_reactivate_limit", args.sparsify_reactivate_limit}}; + nlohmann::json stats_json = { + {"circuit_path", args.circuit_path}, + {"dem_path", args.dem_path}, + {"max_errors", args.max_errors}, + {"sample_seed", args.sample_seed}, + + {"det_beam", args.det_beam}, + {"det_penalty", args.det_penalty}, + {"beam_climbing", args.beam_climbing}, + {"no_revisit_dets", args.no_revisit_dets}, + {"pqlimit", args.pqlimit}, + {"num_det_orders", args.num_det_orders}, + {"det_order_seed", args.det_order_seed}, + {"total_time_seconds", total_time_seconds}, + {"num_errors", num_errors}, + {"num_low_confidence", num_low_confidence}, + {"num_shots", shot}, + {"num_threads", args.num_threads}, + {"sample_num_shots", args.sample_num_shots}, + {"sparsify_errors", args.sparsify_errors}, + {"sparsify_base_degree", args.sparsify_base_degree}, + {"sparsify_max_degree", args.sparsify_max_degree}, + {"sparsify_reactivate_limit", effective_sparsify_reactivate_limit}}; if (args.stats_out_fname == "-") { std::cout << stats_json << std::endl; diff --git a/src/tesseract_sinter_compat.pybind.h b/src/tesseract_sinter_compat.pybind.h index c889423..4778583 100644 --- a/src/tesseract_sinter_compat.pybind.h +++ b/src/tesseract_sinter_compat.pybind.h @@ -109,6 +109,10 @@ struct TesseractSinterDecoder { size_t pqlimit; double det_penalty; bool create_visualization; + bool sparsify_errors; + int sparsify_base_degree; + int sparsify_max_degree; + int sparsify_reactivate_limit; // Parameters for build_det_orders size_t num_det_orders; @@ -125,15 +129,21 @@ struct TesseractSinterDecoder { pqlimit(DEFAULT_PQLIMIT), det_penalty(0.0), create_visualization(false), + sparsify_errors(false), + sparsify_base_degree(-1), + sparsify_max_degree(-1), + sparsify_reactivate_limit(-1), num_det_orders(0), - det_order_method(DetOrder::DetBFS), + det_order_method(DetOrder::DetIndex), 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) + DetOrder det_order_method, uint64_t seed, bool sparsify_errors, + int sparsify_base_degree, int sparsify_max_degree, + int sparsify_reactivate_limit) : det_beam(det_beam), beam_climbing(beam_climbing), no_revisit_dets(no_revisit_dets), @@ -142,6 +152,10 @@ struct TesseractSinterDecoder { pqlimit(pqlimit), det_penalty(det_penalty), create_visualization(create_visualization), + sparsify_errors(sparsify_errors), + sparsify_base_degree(sparsify_base_degree), + sparsify_max_degree(sparsify_max_degree), + sparsify_reactivate_limit(sparsify_reactivate_limit), num_det_orders(num_det_orders), det_order_method(det_order_method), seed(seed) {} @@ -151,6 +165,10 @@ struct TesseractSinterDecoder { 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 && + sparsify_errors == other.sparsify_errors && + sparsify_base_degree == other.sparsify_base_degree && + sparsify_max_degree == other.sparsify_max_degree && + sparsify_reactivate_limit == other.sparsify_reactivate_limit && num_det_orders == other.num_det_orders && det_order_method == other.det_order_method && seed == other.seed; } @@ -166,9 +184,20 @@ struct TesseractSinterDecoder { std::vector> 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}; + TesseractConfig local_config = {stim_dem, + det_beam, + beam_climbing, + no_revisit_dets, + verbose, + merge_errors, + pqlimit, + det_orders, + det_penalty, + create_visualization, + sparsify_errors, + sparsify_base_degree, + sparsify_max_degree, + sparsify_reactivate_limit}; auto decoder = std::make_unique(local_config); return TesseractSinterCompiledDecoder{ @@ -201,9 +230,20 @@ struct TesseractSinterDecoder { std::vector> 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}; + TesseractConfig local_config = {stim_dem, + det_beam, + beam_climbing, + no_revisit_dets, + verbose, + merge_errors, + pqlimit, + det_orders, + det_penalty, + create_visualization, + sparsify_errors, + sparsify_base_degree, + sparsify_max_degree, + sparsify_reactivate_limit}; TesseractDecoder decoder(local_config); // Calculate expected number of bytes per shot for detectors and observables. @@ -304,15 +344,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(), - 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( + .def(py::init(), + 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::DetIndex, + py::arg("seed") = 2384753, py::arg("sparsify_errors") = false, + py::arg("sparsify_base_degree") = -1, py::arg("sparsify_max_degree") = -1, + py::arg("sparsify_reactivate_limit") = -1, + R"pbdoc( Initializes a new TesseractSinterDecoder instance with custom TesseractConfig parameters. )pbdoc") .def("compile_decoder_for_dem", &TesseractSinterDecoder::compile_decoder_for_dem, @@ -347,6 +389,11 @@ void pybind_sinter_compat(py::module& root) { .def_readwrite("pqlimit", &TesseractSinterDecoder::pqlimit) .def_readwrite("det_penalty", &TesseractSinterDecoder::det_penalty) .def_readwrite("create_visualization", &TesseractSinterDecoder::create_visualization) + .def_readwrite("sparsify_errors", &TesseractSinterDecoder::sparsify_errors) + .def_readwrite("sparsify_base_degree", &TesseractSinterDecoder::sparsify_base_degree) + .def_readwrite("sparsify_max_degree", &TesseractSinterDecoder::sparsify_max_degree) + .def_readwrite("sparsify_reactivate_limit", + &TesseractSinterDecoder::sparsify_reactivate_limit) .def_readwrite("num_det_orders", &TesseractSinterDecoder::num_det_orders) .def_readwrite("det_order_method", &TesseractSinterDecoder::det_order_method) .def_readwrite("seed", &TesseractSinterDecoder::seed) @@ -359,16 +406,34 @@ void pybind_sinter_compat(py::module& root) { 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); + self.det_order_method, self.seed, self.sparsify_errors, + self.sparsify_base_degree, self.sparsify_max_degree, + self.sparsify_reactivate_limit); }, [](py::tuple t) { // __setstate__ - if (t.size() != 11) { + if (t.size() == 11) { + return TesseractSinterDecoder( + t[0].cast(), t[1].cast(), t[2].cast(), t[3].cast(), + t[4].cast(), t[5].cast(), t[6].cast(), t[7].cast(), + t[8].cast(), t[9].cast(), t[10].cast(), + /*sparsify_errors=*/false, /*sparsify_base_degree=*/-1, + /*sparsify_max_degree=*/-1, /*sparsify_reactivate_limit=*/-1); + } + if (t.size() != 15) { throw std::runtime_error("Invalid state for TesseractSinterDecoder!"); } + if (py::isinstance(t[8])) { + return TesseractSinterDecoder( + t[0].cast(), t[1].cast(), t[2].cast(), t[3].cast(), + t[4].cast(), t[5].cast(), t[6].cast(), t[7].cast(), + t[12].cast(), t[13].cast(), t[14].cast(), + t[8].cast(), t[9].cast(), t[10].cast(), t[11].cast()); + } return TesseractSinterDecoder( t[0].cast(), t[1].cast(), t[2].cast(), t[3].cast(), t[4].cast(), t[5].cast(), t[6].cast(), t[7].cast(), - t[8].cast(), t[9].cast(), t[10].cast()); + t[8].cast(), t[9].cast(), t[10].cast(), + t[11].cast(), t[12].cast(), t[13].cast(), t[14].cast()); })); // Add a function to create a dictionary of custom decoders @@ -380,13 +445,45 @@ void pybind_sinter_compat(py::module& root) { /*det_beam=*/20, /*beam_climbing=*/true, /*no_revisit_dets=*/true, /*verbose=*/false, /*merge_errors=*/true, /*pqlimit=*/1000000, /*det_penalty=*/0.0, /*create_visualization=*/false, - /*num_det_orders=*/21, /*det_order_method=*/DetOrder::DetIndex, /*seed=*/2384753); + /*num_det_orders=*/21, /*det_order_method=*/DetOrder::DetIndex, /*seed=*/2384753, + /*sparsify_errors=*/false, /*sparsify_base_degree=*/-1, + /*sparsify_max_degree=*/-1, /*sparsify_reactivate_limit=*/-1); result["tesseract"] = result["tesseract-long-beam"]; + result["tesseract-long-beam-sparsify3"] = TesseractSinterDecoder( + /*det_beam=*/20, /*beam_climbing=*/true, /*no_revisit_dets=*/true, + /*verbose=*/false, /*merge_errors=*/true, /*pqlimit=*/1000000, + /*det_penalty=*/0.0, /*create_visualization=*/false, + /*num_det_orders=*/21, /*det_order_method=*/DetOrder::DetIndex, /*seed=*/2384753, + /*sparsify_errors=*/true, /*sparsify_base_degree=*/3, + /*sparsify_max_degree=*/-1, /*sparsify_reactivate_limit=*/-1); + result["tesseract-long-beam-sparsify2"] = TesseractSinterDecoder( + /*det_beam=*/20, /*beam_climbing=*/true, /*no_revisit_dets=*/true, + /*verbose=*/false, /*merge_errors=*/true, /*pqlimit=*/1000000, + /*det_penalty=*/0.0, /*create_visualization=*/false, + /*num_det_orders=*/21, /*det_order_method=*/DetOrder::DetIndex, /*seed=*/2384753, + /*sparsify_errors=*/true, /*sparsify_base_degree=*/2, + /*sparsify_max_degree=*/-1, /*sparsify_reactivate_limit=*/-1); result["tesseract-short-beam"] = TesseractSinterDecoder( /*det_beam=*/15, /*beam_climbing=*/true, /*no_revisit_dets=*/true, /*verbose=*/false, /*merge_errors=*/true, /*pqlimit=*/200000, /*det_penalty=*/0.0, /*create_visualization=*/false, - /*num_det_orders=*/16, /*det_order_method=*/DetOrder::DetIndex, /*seed=*/2384753); + /*num_det_orders=*/16, /*det_order_method=*/DetOrder::DetIndex, /*seed=*/2384753, + /*sparsify_errors=*/false, /*sparsify_base_degree=*/-1, + /*sparsify_max_degree=*/-1, /*sparsify_reactivate_limit=*/-1); + result["tesseract-short-beam-sparsify3"] = TesseractSinterDecoder( + /*det_beam=*/15, /*beam_climbing=*/true, /*no_revisit_dets=*/true, + /*verbose=*/false, /*merge_errors=*/true, /*pqlimit=*/200000, + /*det_penalty=*/0.0, /*create_visualization=*/false, + /*num_det_orders=*/16, /*det_order_method=*/DetOrder::DetIndex, /*seed=*/2384753, + /*sparsify_errors=*/true, /*sparsify_base_degree=*/3, + /*sparsify_max_degree=*/-1, /*sparsify_reactivate_limit=*/-1); + result["tesseract-short-beam-sparsify2"] = TesseractSinterDecoder( + /*det_beam=*/15, /*beam_climbing=*/true, /*no_revisit_dets=*/true, + /*verbose=*/false, /*merge_errors=*/true, /*pqlimit=*/200000, + /*det_penalty=*/0.0, /*create_visualization=*/false, + /*num_det_orders=*/16, /*det_order_method=*/DetOrder::DetIndex, /*seed=*/2384753, + /*sparsify_errors=*/true, /*sparsify_base_degree=*/2, + /*sparsify_max_degree=*/-1, /*sparsify_reactivate_limit=*/-1); return result; }, R"pbdoc( diff --git a/src/utils.h b/src/utils.h index fe89b4f..3c5d656 100644 --- a/src/utils.h +++ b/src/utils.h @@ -41,7 +41,7 @@ enum class DetOrder { DetBFS, DetIndex, DetCoordinate }; std::vector> build_det_orders(const stim::DetectorErrorModel& dem, size_t num_det_orders, - DetOrder method = DetOrder::DetBFS, + DetOrder method = DetOrder::DetIndex, uint64_t seed = 0); const double INF = std::numeric_limits::infinity(); diff --git a/src/utils.pybind.h b/src/utils.pybind.h index 92ba668..118b87e 100644 --- a/src/utils.pybind.h +++ b/src/utils.pybind.h @@ -89,7 +89,7 @@ void add_utils_module(py::module& root) { auto input_dem = parse_py_object(dem); return build_det_orders(input_dem, num_det_orders, method, seed); }, - py::arg("dem"), py::arg("num_det_orders"), py::arg("method") = DetOrder::DetBFS, + py::arg("dem"), py::arg("num_det_orders"), py::arg("method") = DetOrder::DetIndex, py::arg("seed") = 0, R"pbdoc( Generates various detector orderings for decoding. @@ -99,11 +99,11 @@ void add_utils_module(py::module& root) { The detector error model to generate orders for. num_det_orders : int The number of detector orderings to generate. - method : tesseract_decoder.utils.DetOrder, default=tesseract_decoder.utils.DetOrder.DetBFS - Strategy for ordering detectors. ``DetBFS`` performs a breadth-first - traversal, ``DetCoordinate`` uses randomized geometric orientations, - and ``DetIndex`` chooses either increasing or decreasing detector - index order at random. + method : tesseract_decoder.utils.DetOrder, default=tesseract_decoder.utils.DetOrder.DetIndex + Strategy for ordering detectors. ``DetIndex`` chooses either increasing + or decreasing detector index order at random, ``DetBFS`` performs a + breadth-first traversal, and ``DetCoordinate`` uses randomized + geometric orientations. seed : int, default=0 A seed for the random number generator.